@reticlehq/server 2.4.0 → 2.6.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 +719 -0
- package/dist/bridge/auth-failure-reason.d.ts +19 -0
- package/dist/bridge/auth-failure-reason.js +29 -0
- package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
- package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
- package/dist/{bridge.d.ts → bridge/bridge.d.ts} +13 -1
- package/dist/{bridge.js → bridge/bridge.js} +95 -15
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +9 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +18 -10
- package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
- package/dist/capsule/capsule-store.d.ts +8 -3
- package/dist/capsule/capsule-store.js +7 -2
- package/dist/capsule/capsule.js +3 -3
- package/dist/capsule/causal-summary.d.ts +8 -0
- package/dist/capsule/causal-summary.js +22 -12
- package/dist/capsule/divergence.d.ts +9 -4
- package/dist/capsule/divergence.js +8 -8
- package/dist/capsule/predicate-to-links.js +11 -8
- package/dist/cli/cli-doctor.d.ts +1 -0
- package/dist/cli/cli-doctor.js +135 -0
- package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
- package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
- package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
- package/dist/cli/cli-launch.js +171 -0
- package/dist/cli/cli-parse-feedback.d.ts +31 -0
- package/dist/cli/cli-parse-feedback.js +56 -0
- package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +6 -19
- package/dist/{cli-parse.js → cli/cli-parse.js} +118 -81
- package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
- package/dist/{cli-port.js → cli/cli-port.js} +9 -9
- package/dist/cli/cli-update-commands.d.ts +4 -0
- package/dist/cli/cli-update-commands.js +93 -0
- package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
- package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
- package/dist/cli/doctor-daemon-line.d.ts +37 -0
- package/dist/cli/doctor-daemon-line.js +35 -0
- package/dist/cli/port-holder.d.ts +43 -0
- package/dist/cli/port-holder.js +99 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +302 -100
- package/dist/cloud/cloud-config.d.ts +0 -1
- package/dist/cloud/cloud-config.js +9 -9
- package/dist/cloud/cloud-sync.d.ts +1 -7
- package/dist/cloud/cloud-sync.js +12 -12
- package/dist/crawl/crawl-empty.d.ts +16 -0
- package/dist/crawl/crawl-empty.js +24 -0
- package/dist/crawl/crawl-tools.js +4 -1
- package/dist/crawl/crawl.js +26 -7
- package/dist/daemon/daemon-resilience.d.ts +73 -0
- package/dist/daemon/daemon-resilience.js +279 -0
- package/dist/daemon/daemon-usefulness.d.ts +61 -0
- package/dist/daemon/daemon-usefulness.js +62 -0
- package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
- package/dist/{daemon.js → daemon/daemon.js} +105 -48
- package/dist/daemon/heartbeat.d.ts +115 -0
- package/dist/daemon/heartbeat.js +141 -0
- package/dist/daemon/idle-grace.d.ts +32 -0
- package/dist/daemon/idle-grace.js +41 -0
- package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +23 -2
- package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +37 -6
- package/dist/daemon/port-presence.d.ts +76 -0
- package/dist/daemon/port-presence.js +93 -0
- package/dist/daemon/state-dir.d.ts +7 -0
- package/dist/daemon/state-dir.js +59 -0
- package/dist/daemon/unclean-predecessor.d.ts +23 -0
- package/dist/daemon/unclean-predecessor.js +27 -0
- package/dist/domain/domain-model.js +2 -2
- package/dist/events/body-failures.js +8 -8
- package/dist/events/contradictions.d.ts +1 -1
- package/dist/events/contradictions.js +60 -10
- package/dist/events/echo-mismatch.js +10 -10
- package/dist/events/event-filters.js +6 -6
- package/dist/events/json-salvage.d.ts +2 -1
- package/dist/events/json-salvage.js +6 -6
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +21 -0
- package/dist/events/predicate-eval.d.ts +53 -20
- package/dist/events/predicate-eval.js +179 -55
- package/dist/events/predicate-parse.d.ts +23 -0
- package/dist/events/predicate-parse.js +54 -0
- package/dist/events/predicate.d.ts +7 -2
- package/dist/events/predicate.js +126 -35
- package/dist/events/reconcile.js +5 -5
- package/dist/events/ring-buffer.js +2 -17
- package/dist/events/stale-response.js +6 -6
- package/dist/events/testid-near-miss.d.ts +22 -0
- package/dist/events/testid-near-miss.js +33 -0
- package/dist/events/unit-mismatch.js +8 -8
- package/dist/flows/affected-tools.js +19 -4
- package/dist/flows/affected.js +1 -1
- package/dist/flows/annotate-target.d.ts +1 -0
- package/dist/flows/annotate-target.js +20 -0
- package/dist/flows/annotate-tools.js +25 -3
- package/dist/flows/annotate.js +17 -4
- package/dist/flows/assertion-integrity.d.ts +2 -1
- package/dist/flows/assertion-tiers-store.d.ts +3 -66
- package/dist/flows/assertion-tiers-store.js +2 -2
- package/dist/flows/attributed-failure.d.ts +17 -0
- package/dist/flows/attributed-failure.js +20 -0
- package/dist/flows/buddy-status.d.ts +2 -1
- package/dist/flows/buddy-status.js +2 -2
- package/dist/flows/coverage.d.ts +3 -2
- package/dist/flows/coverage.js +2 -2
- package/dist/flows/decision.js +24 -9
- package/dist/flows/empty-flow.d.ts +6 -0
- package/dist/flows/empty-flow.js +24 -0
- package/dist/flows/flake-store.d.ts +0 -1
- package/dist/flows/flake-store.js +1 -1
- package/dist/flows/flake.d.ts +2 -1
- package/dist/flows/flake.js +2 -2
- package/dist/flows/flow-classify.js +12 -0
- package/dist/flows/flow-replay-run.js +4 -4
- package/dist/flows/flow-replay.d.ts +6 -8
- package/dist/flows/flow-replay.js +132 -48
- package/dist/flows/flow-report.js +1 -1
- package/dist/flows/flow-scope.d.ts +1 -6
- package/dist/flows/flow-step-runners.d.ts +26 -6
- package/dist/flows/flow-step-runners.js +156 -6
- package/dist/flows/flow-success.js +20 -15
- package/dist/flows/flow-tools.js +63 -21
- package/dist/flows/flows.js +15 -11
- package/dist/flows/gate.d.ts +4 -3
- package/dist/flows/gate.js +1 -1
- package/dist/flows/heal-run.d.ts +0 -8
- package/dist/flows/heal-run.js +5 -5
- package/dist/flows/heal.js +3 -3
- package/dist/flows/predicate-to-expect.d.ts +38 -0
- package/dist/flows/predicate-to-expect.js +95 -0
- package/dist/flows/replay.d.ts +16 -1
- package/dist/flows/replay.js +58 -22
- package/dist/flows/role-anchor-nearest.d.ts +6 -0
- package/dist/flows/role-anchor-nearest.js +45 -0
- package/dist/flows/role-drift-reason.d.ts +20 -0
- package/dist/flows/role-drift-reason.js +39 -0
- package/dist/flows/server-verify.d.ts +1 -1
- package/dist/flows/server-verify.js +5 -5
- package/dist/flows/suite-flakes.d.ts +33 -0
- package/dist/flows/suite-flakes.js +32 -0
- package/dist/flows/verify-change-tools.js +32 -10
- package/dist/flows/verify-change-verdict.d.ts +15 -0
- package/dist/flows/verify-change-verdict.js +25 -0
- package/dist/flows/watch-batcher.d.ts +3 -2
- package/dist/honesty/already-true.d.ts +2 -0
- package/dist/honesty/already-true.js +31 -0
- package/dist/honesty/blind-spots.d.ts +35 -3
- package/dist/honesty/blind-spots.js +57 -12
- package/dist/honesty/honesty.d.ts +3 -2
- package/dist/honesty/honesty.js +4 -4
- package/dist/honesty/uncaptured-bodies.js +1 -1
- package/dist/honesty/unread-outcome.js +2 -2
- package/dist/honesty/verified.d.ts +31 -3
- package/dist/honesty/verified.js +64 -8
- package/dist/http-server.js +23 -7
- package/dist/hunt/hunt-report.d.ts +2 -1
- package/dist/hunt/hunt-report.js +2 -2
- package/dist/index.d.ts +9 -6
- package/dist/index.js +41 -25
- package/dist/init/agent-rules.d.ts +20 -1
- package/dist/init/agent-rules.js +64 -8
- package/dist/init/app-choice.d.ts +20 -0
- package/dist/init/app-choice.js +25 -0
- package/dist/init/astro-host.d.ts +21 -0
- package/dist/init/astro-host.js +27 -0
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +240 -0
- package/dist/init/capabilities.js +4 -1
- package/dist/init/cra.d.ts +50 -0
- package/dist/init/cra.js +106 -0
- package/dist/init/cursor.js +39 -4
- package/dist/init/desktop-doctor.d.ts +3 -2
- package/dist/init/desktop-doctor.js +4 -4
- package/dist/init/detect.d.ts +9 -2
- package/dist/init/detect.js +24 -5
- package/dist/init/mcp-clients.d.ts +81 -0
- package/dist/init/mcp-clients.js +248 -0
- package/dist/init/mcp-registered.d.ts +12 -0
- package/dist/init/mcp-registered.js +14 -0
- package/dist/init/mcp.d.ts +27 -2
- package/dist/init/mcp.js +40 -4
- package/dist/init/next-patch.d.ts +2 -4
- package/dist/init/next-patch.js +109 -19
- package/dist/init/node-io.js +29 -6
- package/dist/init/plan-framework.d.ts +16 -1
- package/dist/init/plan-framework.js +166 -8
- package/dist/init/plan.d.ts +85 -6
- package/dist/init/plan.js +196 -33
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +23 -1
- package/dist/init/run.js +338 -47
- package/dist/init/slash-command.d.ts +9 -0
- package/dist/init/slash-command.js +10 -1
- package/dist/init/snippets.d.ts +7 -10
- package/dist/init/snippets.js +44 -15
- package/dist/init/vite-config.js +1 -1
- package/dist/init/windows-quote.d.ts +38 -0
- package/dist/init/windows-quote.js +70 -0
- package/dist/init/workspace-apps.d.ts +23 -0
- package/dist/init/workspace-apps.js +68 -0
- package/dist/input/driven-redaction.js +1 -1
- package/dist/input/network-detail.js +8 -8
- package/dist/input/network-mock-tools.js +2 -2
- package/dist/input/network-mock.js +4 -4
- package/dist/input/real-input.js +8 -3
- package/dist/input/scroll-find.js +2 -2
- package/dist/input/scroll-tools.d.ts +0 -5
- package/dist/input/scroll-tools.js +11 -4
- package/dist/input/viewport-tools.js +2 -2
- package/dist/journal/ambient-store.d.ts +0 -2
- package/dist/journal/ambient-store.js +1 -1
- package/dist/journal/ambient.js +1 -1
- package/dist/journal/attach-journal.d.ts +2 -1
- package/dist/journal/deviation-report.js +6 -6
- package/dist/journal/deviation-service.js +1 -1
- package/dist/journal/envelope-store.d.ts +0 -2
- package/dist/journal/envelope-store.js +1 -1
- package/dist/journal/envelope.d.ts +2 -3
- package/dist/journal/envelope.js +3 -3
- package/dist/journal/journal-query.js +1 -1
- package/dist/journal/journal-recorder.d.ts +2 -1
- package/dist/journal/journal-recorder.js +2 -2
- package/dist/journal/rollups.js +9 -9
- package/dist/journal/session-end.d.ts +2 -1
- package/dist/journal/session-journal.js +4 -4
- package/dist/license/license.d.ts +0 -14
- package/dist/license/license.js +8 -8
- package/dist/log.d.ts +10 -0
- package/dist/log.js +11 -1
- package/dist/mcp/mcp-is-error.d.ts +13 -0
- package/dist/mcp/mcp-is-error.js +18 -0
- package/dist/mcp/mcp-outage.d.ts +32 -0
- package/dist/mcp/mcp-outage.js +55 -0
- package/dist/mcp/mcp-proxy.d.ts +134 -0
- package/dist/mcp/mcp-proxy.js +720 -0
- package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
- package/dist/{mcp.js → mcp/mcp.js} +139 -37
- package/dist/mcp/proxy-handshake.d.ts +3 -0
- package/dist/mcp/proxy-handshake.js +54 -0
- package/dist/mcp/proxy-lifecycle.d.ts +64 -0
- package/dist/mcp/proxy-lifecycle.js +70 -0
- package/dist/mcp/tool-catalog-cache.d.ts +29 -0
- package/dist/mcp/tool-catalog-cache.js +55 -0
- package/dist/oracles/propose-consequences.d.ts +2 -1
- package/dist/oracles/propose-consequences.js +16 -11
- package/dist/oracles/self-instrument.d.ts +2 -1
- package/dist/oracles/self-instrument.js +1 -1
- package/dist/phenomena/phenomena.js +3 -3
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +16 -0
- package/dist/pool/browser-pool.d.ts +9 -0
- package/dist/pool/browser-pool.js +38 -9
- package/dist/pool/playwright-launcher.d.ts +15 -2
- package/dist/pool/playwright-launcher.js +21 -2
- package/dist/project/fs-port.js +1 -1
- package/dist/project/project-tools.js +2 -2
- package/dist/runs/build-verification-run.js +1 -1
- package/dist/runs/repair-prompt.d.ts +0 -6
- package/dist/runs/repair-prompt.js +3 -3
- package/dist/runs/risk-classify.js +1 -1
- package/dist/runs/run-diff.d.ts +0 -2
- package/dist/runs/run-diff.js +3 -3
- package/dist/runs/run-tools.js +2 -2
- package/dist/runs/verification-sync.js +2 -2
- package/dist/runs/verify-http.js +1 -1
- package/dist/runs/verify-server.js +1 -1
- package/dist/session/attachment-history.d.ts +46 -0
- package/dist/session/attachment-history.js +65 -0
- package/dist/session/command-timeout.d.ts +11 -3
- package/dist/session/command-timeout.js +2 -2
- package/dist/session/control-envelope.d.ts +6 -0
- package/dist/session/control-envelope.js +7 -1
- package/dist/session/dev-server-probe.d.ts +13 -0
- package/dist/session/dev-server-probe.js +52 -0
- package/dist/session/last-act.d.ts +17 -3
- package/dist/session/last-act.js +16 -4
- package/dist/session/live-control-tools.js +7 -2
- package/dist/session/live-control.js +1 -1
- package/dist/session/looks-like-dev-server.d.ts +14 -0
- package/dist/session/looks-like-dev-server.js +25 -0
- package/dist/session/no-session-diagnosis.d.ts +45 -0
- package/dist/session/no-session-diagnosis.js +120 -0
- package/dist/session/no-session-watch.d.ts +21 -0
- package/dist/session/no-session-watch.js +69 -0
- package/dist/session/observed-state.d.ts +4 -0
- package/dist/session/observed-state.js +19 -1
- package/dist/session/output-budget.js +1 -1
- package/dist/session/pending-commands.js +1 -0
- package/dist/session/session-health.d.ts +8 -1
- package/dist/session/session-health.js +8 -7
- package/dist/session/session-info.d.ts +43 -8
- package/dist/session/session-info.js +46 -1
- package/dist/session/session-lease.d.ts +0 -11
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +6 -0
- package/dist/session/session-manager.js +124 -12
- package/dist/session/session-readiness.d.ts +1 -1
- package/dist/session/session-readiness.js +6 -1
- package/dist/session/session-reaper.js +1 -1
- package/dist/session/session-reconnect.d.ts +32 -0
- package/dist/session/session-reconnect.js +42 -0
- package/dist/session/session.d.ts +15 -2
- package/dist/session/session.js +77 -26
- package/dist/session/state-select.d.ts +1 -1
- package/dist/session/state-select.js +1 -1
- package/dist/status-payload.d.ts +19 -0
- package/dist/status-payload.js +11 -0
- package/dist/telemetry/argument-shape.js +10 -1
- package/dist/telemetry/browser-mode.d.ts +27 -0
- package/dist/telemetry/browser-mode.js +33 -0
- package/dist/telemetry/bug-found.d.ts +17 -21
- package/dist/telemetry/bug-found.js +120 -8
- package/dist/telemetry/cli-telemetry.d.ts +2 -12
- package/dist/telemetry/cli-telemetry.js +27 -4
- package/dist/telemetry/crash-cause.d.ts +47 -0
- package/dist/telemetry/crash-cause.js +104 -0
- package/dist/telemetry/daemon-telemetry.d.ts +38 -1
- package/dist/telemetry/daemon-telemetry.js +37 -12
- package/dist/telemetry/dev-repo.js +4 -4
- package/dist/telemetry/error-class.d.ts +26 -0
- package/dist/telemetry/error-class.js +52 -0
- package/dist/telemetry/error-fingerprint.js +21 -2
- package/dist/telemetry/feedback-cli.d.ts +8 -3
- package/dist/telemetry/feedback-cli.js +52 -8
- package/dist/telemetry/feedback-context.d.ts +23 -6
- package/dist/telemetry/feedback-context.js +105 -5
- package/dist/telemetry/feedback-delivery.d.ts +20 -0
- package/dist/telemetry/feedback-delivery.js +30 -0
- package/dist/telemetry/feedback-outbox.d.ts +22 -0
- package/dist/telemetry/feedback-outbox.js +87 -0
- package/dist/telemetry/feedback.d.ts +22 -1
- package/dist/telemetry/feedback.js +67 -8
- package/dist/telemetry/git-facts.d.ts +7 -0
- package/dist/telemetry/git-facts.js +5 -5
- package/dist/telemetry/project-profile.js +2 -1
- package/dist/telemetry/session-metrics.d.ts +47 -3
- package/dist/telemetry/session-metrics.js +315 -15
- package/dist/telemetry/telemetry.d.ts +45 -2
- package/dist/telemetry/telemetry.js +150 -37
- package/dist/telemetry/verification-of.d.ts +22 -0
- package/dist/telemetry/verification-of.js +84 -0
- package/dist/tools/act-capsule.js +2 -2
- package/dist/tools/act-danger.d.ts +12 -0
- package/dist/tools/act-danger.js +20 -3
- package/dist/tools/act-helpers.js +4 -4
- package/dist/tools/act-tools.d.ts +0 -8
- package/dist/tools/act-tools.js +114 -118
- package/dist/tools/act-view.js +7 -7
- package/dist/tools/alias-args.d.ts +17 -0
- package/dist/tools/alias-args.js +25 -0
- package/dist/tools/assert-grade.js +9 -9
- package/dist/tools/assert-verdict.d.ts +9 -1
- package/dist/tools/assert-verdict.js +21 -6
- package/dist/tools/browser-tools.js +36 -11
- package/dist/tools/contract-tools.d.ts +0 -5
- package/dist/tools/contract-tools.js +27 -1
- package/dist/tools/coverage-identity.d.ts +34 -0
- package/dist/tools/coverage-identity.js +46 -0
- package/dist/tools/coverage-tools.js +10 -14
- package/dist/tools/dynamic-tools.d.ts +2 -1
- package/dist/tools/dynamic-tools.js +96 -19
- package/dist/tools/error-recovery.d.ts +34 -2
- package/dist/tools/error-recovery.js +255 -4
- package/dist/tools/feedback-invite.d.ts +48 -0
- package/dist/tools/feedback-invite.js +71 -0
- package/dist/tools/feedback-tools.d.ts +12 -1
- package/dist/tools/feedback-tools.js +28 -5
- package/dist/tools/invoke-tool.d.ts +0 -12
- package/dist/tools/invoke-tool.js +126 -33
- package/dist/tools/lease-hint.d.ts +17 -0
- package/dist/tools/lease-hint.js +24 -0
- package/dist/tools/lease-tools.js +6 -5
- package/dist/tools/merge-tools.d.ts +10 -1
- package/dist/tools/merge-tools.js +9 -3
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +55 -0
- package/dist/tools/navigate-arrival.d.ts +26 -0
- package/dist/tools/navigate-arrival.js +60 -0
- package/dist/tools/navigate-result.d.ts +31 -0
- package/dist/tools/navigate-result.js +38 -0
- package/dist/tools/observe-tools.js +72 -20
- package/dist/tools/observed-nothing.d.ts +25 -0
- package/dist/tools/observed-nothing.js +33 -0
- package/dist/tools/query-paginate.js +2 -2
- package/dist/tools/query-shape.d.ts +18 -0
- package/dist/tools/query-shape.js +39 -0
- package/dist/tools/read-tools.js +81 -20
- package/dist/tools/real-input-attempt.d.ts +28 -0
- package/dist/tools/real-input-attempt.js +93 -0
- package/dist/tools/reconcile-tools.js +7 -7
- package/dist/tools/reload-result.d.ts +14 -0
- package/dist/tools/reload-result.js +29 -0
- package/dist/tools/snapshot-delta.d.ts +4 -5
- package/dist/tools/snapshot-delta.js +0 -0
- package/dist/tools/tool-kit.d.ts +16 -0
- package/dist/tools/tool-surface.d.ts +83 -0
- package/dist/tools/tool-surface.js +217 -0
- package/dist/tools/tools-helpers.js +6 -6
- package/dist/tools/tools.d.ts +15 -0
- package/dist/tools/tools.js +53 -17
- package/dist/tools/unadvertised-help.d.ts +5 -0
- package/dist/tools/unadvertised-help.js +39 -0
- package/dist/trace.d.ts +69 -0
- package/dist/trace.js +150 -0
- package/dist/update/nudge-credit.d.ts +4 -0
- package/dist/update/nudge-credit.js +53 -0
- package/dist/update/sdk-sync.d.ts +21 -0
- package/dist/update/sdk-sync.js +42 -0
- package/dist/update/update-checker.js +3 -3
- package/dist/update/update-nudge.d.ts +63 -6
- package/dist/update/update-nudge.js +128 -15
- package/dist/update/updater.d.ts +1 -9
- package/dist/update/updater.js +13 -8
- package/dist/version/peer-announce.d.ts +4 -0
- package/dist/version/peer-announce.js +30 -0
- package/dist/{server-version.js → version/server-version.js} +5 -1
- package/dist/version/version-nudge.d.ts +28 -0
- package/dist/version/version-nudge.js +40 -0
- package/dist/version/version-skew.d.ts +66 -0
- package/dist/version/version-skew.js +71 -0
- package/dist/visual/visual-diff.js +2 -2
- package/dist/visual/visual-tools.js +4 -4
- package/docs/README.md +55 -0
- package/docs/agent-cheatsheet.md +85 -0
- package/docs/architecture.md +113 -0
- package/docs/benchmarks.md +255 -0
- package/docs/debugging.md +102 -0
- package/docs/deploy-checks.md +69 -0
- package/docs/desktop-apps.md +256 -0
- package/docs/enterprise.md +62 -0
- package/docs/fixtures-dispatch-receiver.yml +76 -0
- package/docs/fixtures.md +45 -0
- package/docs/flows.md +166 -0
- package/docs/gate-plan.md +98 -0
- package/docs/gates.md +117 -0
- package/docs/getting-started.md +469 -0
- package/docs/human-control.md +63 -0
- package/docs/integration-patterns.md +171 -0
- package/docs/local-registry.md +126 -0
- package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
- package/docs/matrix/2.5.0/codex-machine.json +20 -0
- package/docs/matrix/2.5.0/cursor-machine.json +22 -0
- package/docs/matrix/2.5.0/gemini-machine.json +22 -0
- package/docs/matrix/2.5.0/opencode-machine.json +22 -0
- package/docs/matrix/2.5.0/vscode-machine.json +22 -0
- package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
- package/docs/matrix/MATRIX.md +26 -0
- package/docs/matrix/README.md +59 -0
- package/docs/mint.json +61 -0
- package/docs/multi-agent-testing.md +55 -0
- package/docs/platform-integration.md +122 -0
- package/docs/system-map.md +152 -0
- package/docs/telemetry-contract.md +214 -0
- package/docs/telemetry.md +191 -0
- package/docs/testing.md +82 -0
- package/docs/token-efficiency.md +80 -0
- package/docs/usage.md +1046 -0
- package/package.json +8 -5
- package/dist/cli-doctor.d.ts +0 -6
- package/dist/cli-doctor.js +0 -60
- package/dist/cli-launch.js +0 -112
- package/dist/daemon-resilience.d.ts +0 -22
- package/dist/daemon-resilience.js +0 -85
- package/dist/mcp-proxy.d.ts +0 -67
- package/dist/mcp-proxy.js +0 -337
- package/dist/tools/profiles.d.ts +0 -37
- package/dist/tools/profiles.js +0 -139
- /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
- /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
- /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
- /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
- /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
- /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
package/dist/cloud/cloud-sync.js
CHANGED
|
@@ -15,18 +15,18 @@ export const CloudEnv = {
|
|
|
15
15
|
KEY: 'RETICLE_CLOUD_KEY',
|
|
16
16
|
};
|
|
17
17
|
/** Paths the OSS server pushes to (match the cloud app's contract). */
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const CLOUD_FLOWS_PATH = '/v1/flows';
|
|
19
|
+
const CLOUD_RUNS_PATH = '/v1/runs';
|
|
20
|
+
const CLOUD_PROJECT_RUNS_PATH = '/v1/project/runs';
|
|
21
|
+
const CLOUD_PROJECT_REGRESSION_PATH = '/v1/project/regression';
|
|
22
|
+
const CLOUD_VERIFICATIONS_PATH = '/v1/verifications';
|
|
23
23
|
/** Resolve cloud credentials from the environment, or null when not logged in (sync disabled). */
|
|
24
24
|
export function resolveCloudConfig(env) {
|
|
25
25
|
const url = env[CloudEnv.URL];
|
|
26
26
|
const apiKey = env[CloudEnv.KEY];
|
|
27
|
-
if (typeof url !== 'string' || url.length
|
|
27
|
+
if (typeof url !== 'string' || 0 === url.length)
|
|
28
28
|
return null;
|
|
29
|
-
if (typeof apiKey !== 'string' || apiKey.length
|
|
29
|
+
if (typeof apiKey !== 'string' || 0 === apiKey.length)
|
|
30
30
|
return null;
|
|
31
31
|
return { url: url.replace(/\/+$/, ''), apiKey };
|
|
32
32
|
}
|
|
@@ -40,7 +40,7 @@ export const SyncOutcome = {
|
|
|
40
40
|
* throws — a failure is reported, not propagated, so the local save is authoritative.
|
|
41
41
|
*/
|
|
42
42
|
export async function syncFlowToCloud(flow, config, projectId, fetchImpl) {
|
|
43
|
-
if (
|
|
43
|
+
if (null === config)
|
|
44
44
|
return { outcome: SyncOutcome.SKIPPED };
|
|
45
45
|
try {
|
|
46
46
|
const res = await fetchImpl(`${config.url}${CLOUD_FLOWS_PATH}`, {
|
|
@@ -67,7 +67,7 @@ export async function syncFlowToCloud(flow, config, projectId, fetchImpl) {
|
|
|
67
67
|
* built it), so the body is the run itself, not a wrapper. Best-effort: never throws, never blocks exit.
|
|
68
68
|
*/
|
|
69
69
|
export async function syncRunToCloud(run, config, fetchImpl) {
|
|
70
|
-
if (
|
|
70
|
+
if (null === config)
|
|
71
71
|
return { outcome: SyncOutcome.SKIPPED };
|
|
72
72
|
try {
|
|
73
73
|
const res = await fetchImpl(`${config.url}${CLOUD_RUNS_PATH}`, {
|
|
@@ -93,7 +93,7 @@ export async function syncRunToCloud(run, config, fetchImpl) {
|
|
|
93
93
|
* truth). `projectId` scopes an org's multiple suites. Best-effort: never throws, never blocks the tool.
|
|
94
94
|
*/
|
|
95
95
|
export async function syncRunRecordToCloud(record, projectId, config, fetchImpl) {
|
|
96
|
-
if (
|
|
96
|
+
if (null === config)
|
|
97
97
|
return { outcome: SyncOutcome.SKIPPED };
|
|
98
98
|
try {
|
|
99
99
|
const res = await fetchImpl(`${config.url}${CLOUD_PROJECT_RUNS_PATH}`, {
|
|
@@ -127,7 +127,7 @@ export async function syncRunRecordToCloud(record, projectId, config, fetchImpl)
|
|
|
127
127
|
* cloud), or null when not logged in / unreachable — so the caller degrades to local-only, never fails.
|
|
128
128
|
*/
|
|
129
129
|
export async function fetchProjectRegressionFromCloud(config, projectId, fetchImpl) {
|
|
130
|
-
if (
|
|
130
|
+
if (null === config)
|
|
131
131
|
return null;
|
|
132
132
|
try {
|
|
133
133
|
const query = projectId === undefined ? '' : `?projectId=${encodeURIComponent(projectId)}`;
|
|
@@ -156,7 +156,7 @@ const ServerVerificationSchema = z.object({
|
|
|
156
156
|
* submit fails, so the caller falls back to a local replay. Never throws.
|
|
157
157
|
*/
|
|
158
158
|
export async function submitServerVerification(body, config, fetchImpl) {
|
|
159
|
-
if (
|
|
159
|
+
if (null === config)
|
|
160
160
|
return null;
|
|
161
161
|
try {
|
|
162
162
|
const res = await fetchImpl(`${config.url}${CLOUD_VERIFICATIONS_PATH}`, {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a crawl clicked nothing.
|
|
3
|
+
*
|
|
4
|
+
* `stepsRun: 0` came back with no `error` and nothing else, so "the page had no controls" and "it had
|
|
5
|
+
* 34 and none were clicked" were the same answer. Reported from a sweep on a page with 34 of them,
|
|
6
|
+
* and not reproducible here — crawl drives the bench app correctly — so the honest move is to make
|
|
7
|
+
* the zero carry its own diagnosis rather than guess at a cause.
|
|
8
|
+
*/
|
|
9
|
+
interface CrawlOutcome {
|
|
10
|
+
interactiveFound: number;
|
|
11
|
+
stepsRun: number;
|
|
12
|
+
maxSteps: number;
|
|
13
|
+
truncated: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function crawlEmptyNote(outcome: CrawlOutcome): string | undefined;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a crawl clicked nothing.
|
|
3
|
+
*
|
|
4
|
+
* `stepsRun: 0` came back with no `error` and nothing else, so "the page had no controls" and "it had
|
|
5
|
+
* 34 and none were clicked" were the same answer. Reported from a sweep on a page with 34 of them,
|
|
6
|
+
* and not reproducible here — crawl drives the bench app correctly — so the honest move is to make
|
|
7
|
+
* the zero carry its own diagnosis rather than guess at a cause.
|
|
8
|
+
*/
|
|
9
|
+
export function crawlEmptyNote(outcome) {
|
|
10
|
+
if (outcome.stepsRun > 0)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (outcome.maxSteps <= 0) {
|
|
13
|
+
return 'nothing was clicked because maxSteps is 0 — raise it to crawl at all';
|
|
14
|
+
}
|
|
15
|
+
if (0 === outcome.interactiveFound) {
|
|
16
|
+
const capped = outcome.truncated
|
|
17
|
+
? ' The snapshot was also truncated, so controls past the cap were never seen — narrow `scope` and retry.'
|
|
18
|
+
: '';
|
|
19
|
+
return `no interactive controls were found on this page, so nothing was clicked — the crawl ran and found none, which is a result rather than a failure.${capped}`;
|
|
20
|
+
}
|
|
21
|
+
return (`${String(outcome.interactiveFound)} interactive controls were found and none of them were clicked. ` +
|
|
22
|
+
`That is not an empty page, so something stopped the loop — report it with reticle_feedback, ` +
|
|
23
|
+
`including this page's URL and one control's ref.`);
|
|
24
|
+
}
|
|
@@ -37,6 +37,9 @@ export const CRAWL_TOOLS = [
|
|
|
37
37
|
outputSchema: {
|
|
38
38
|
interactiveFound: z.number(),
|
|
39
39
|
stepsRun: z.number(),
|
|
40
|
+
// Present only when nothing was clicked, saying WHICH zero this is — an empty page, a step
|
|
41
|
+
// budget of zero, or controls found and none driven. A bare zero was unactionable.
|
|
42
|
+
note: z.string().optional(),
|
|
40
43
|
anomalies: z.array(z.object({
|
|
41
44
|
kind: z.string(),
|
|
42
45
|
ref: z.string(),
|
|
@@ -64,7 +67,7 @@ export const CRAWL_TOOLS = [
|
|
|
64
67
|
...(maxSteps !== undefined ? { maxSteps } : {}),
|
|
65
68
|
...(settleMs !== undefined ? { settleMs } : {}),
|
|
66
69
|
...(scope !== undefined ? { scope } : {}),
|
|
67
|
-
...(args['confirmDangerous']
|
|
70
|
+
...(true === args['confirmDangerous'] ? { confirmDangerous: true } : {}),
|
|
68
71
|
};
|
|
69
72
|
return crawl(session, opts, nodeSleep);
|
|
70
73
|
},
|
package/dist/crawl/crawl.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { span } from '../trace.js';
|
|
1
2
|
import { ActionType, CRAWL_DEFAULTS, CrawlAnomalyKind, ContradictionKind, DANGEROUS_ACTION_CONFIRM_ARG, EventType, ReticleCommand, } from '@reticlehq/core';
|
|
3
|
+
import { crawlEmptyNote } from './crawl-empty.js';
|
|
2
4
|
import { parseInteractive, asRecord, asNumber, asString, sourceOf, } from '../tools/tools-helpers.js';
|
|
3
5
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
4
6
|
import { findContradictions } from '../events/contradictions.js';
|
|
@@ -62,7 +64,7 @@ export async function crawl(session, opts, sleep) {
|
|
|
62
64
|
// the controls it never reached are not merely unclicked — they were never seen. Reporting
|
|
63
65
|
// `interactiveFound` without this would state a control count that is really a cap, and
|
|
64
66
|
// `truncated:false` would positively assert that nothing was cut.
|
|
65
|
-
const coverageCapped = snapshot.truncated
|
|
67
|
+
const coverageCapped = true === snapshot.truncated;
|
|
66
68
|
const anomalies = [];
|
|
67
69
|
const visited = [];
|
|
68
70
|
const counts = { consoleErrors: 0, failedRequests: 0, deadControls: 0, contradictions: 0 };
|
|
@@ -83,12 +85,18 @@ export async function crawl(session, opts, sleep) {
|
|
|
83
85
|
session.beginAction?.(ReticleTool.CRAWL, { ref: item.ref, action: ActionType.CLICK });
|
|
84
86
|
let act;
|
|
85
87
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// One span per control clicked, so a slow crawl names the control rather than reporting a
|
|
89
|
+
// single multi-second total. The settle sleep below is INSIDE it deliberately: it is part of
|
|
90
|
+
// what a step costs, and hiding it would make the fixed budget look free.
|
|
91
|
+
act = await span('crawl.step', { ref: item.ref, desc: item.desc }, async () => {
|
|
92
|
+
const clicked = await session.command(ReticleCommand.ACT, {
|
|
93
|
+
ref: item.ref,
|
|
94
|
+
action: ActionType.CLICK,
|
|
95
|
+
args: true === opts.confirmDangerous ? { [DANGEROUS_ACTION_CONFIRM_ARG]: true } : {},
|
|
96
|
+
});
|
|
97
|
+
await sleep(settleMs);
|
|
98
|
+
return clicked;
|
|
90
99
|
});
|
|
91
|
-
await sleep(settleMs);
|
|
92
100
|
}
|
|
93
101
|
finally {
|
|
94
102
|
// Close on every exit so a throw cannot leak the window onto the next control's events.
|
|
@@ -147,7 +155,7 @@ export async function crawl(session, opts, sleep) {
|
|
|
147
155
|
// noise for the false negative this check exists to catch.
|
|
148
156
|
const dispatched = asRecord(act.result)['dispatched'] !== false && act.ok;
|
|
149
157
|
if (dispatched &&
|
|
150
|
-
errs.length
|
|
158
|
+
0 === errs.length &&
|
|
151
159
|
!events.some(isActivity) &&
|
|
152
160
|
!legitimatelyInert(item.desc)) {
|
|
153
161
|
counts.deadControls += 1;
|
|
@@ -171,5 +179,16 @@ export async function crawl(session, opts, sleep) {
|
|
|
171
179
|
// partial sweep into "all controls healthy".
|
|
172
180
|
truncated: items.length > stepsRun || coverageCapped,
|
|
173
181
|
...(coverageCapped ? { coverageNote: CAPPED_SNAPSHOT_NOTE } : {}),
|
|
182
|
+
// A bare `stepsRun: 0` made "the page had no controls" and "it had 34 and none were clicked" the
|
|
183
|
+
// same answer. See crawl-empty.
|
|
184
|
+
...(() => {
|
|
185
|
+
const note = crawlEmptyNote({
|
|
186
|
+
interactiveFound: items.length,
|
|
187
|
+
stepsRun,
|
|
188
|
+
maxSteps,
|
|
189
|
+
truncated: coverageCapped,
|
|
190
|
+
});
|
|
191
|
+
return note === undefined ? {} : { note };
|
|
192
|
+
})(),
|
|
174
193
|
};
|
|
175
194
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-level resilience for the long-running daemon. The daemon serves many agents at once, so a
|
|
3
|
+
* single stray error must not take the whole fleet down:
|
|
4
|
+
*
|
|
5
|
+
* - unhandledRejection (a fire-and-forget promise nobody awaited — the common case in async WS/pool
|
|
6
|
+
* code) → LOG and keep running. One agent's async slip-up can't crash the daemon for everyone.
|
|
7
|
+
* - uncaughtException (a synchronous throw escaped all try/catch) → the process state is undefined
|
|
8
|
+
* per Node's guidance, so LOG a clear reason and exit cleanly; the next `reticle mcp` respawns a fresh
|
|
9
|
+
* daemon, which beats crashing silently or limping along corrupt.
|
|
10
|
+
*/
|
|
11
|
+
export interface ProcessLike {
|
|
12
|
+
on(event: string, listener: (arg: unknown) => void): unknown;
|
|
13
|
+
}
|
|
14
|
+
type LogFn = (event: string, data: Record<string, unknown>) => void;
|
|
15
|
+
/** The two ways a failure reaches the top of the process. Named so the analytics can tell them apart. */
|
|
16
|
+
export declare const CrashKind: {
|
|
17
|
+
readonly UNHANDLED_REJECTION: "unhandled_rejection";
|
|
18
|
+
readonly UNCAUGHT_EXCEPTION: "uncaught_exception";
|
|
19
|
+
};
|
|
20
|
+
export type CrashKind = (typeof CrashKind)[keyof typeof CrashKind];
|
|
21
|
+
/**
|
|
22
|
+
* Log EVERY way this process can leave, so a death always leaves a line.
|
|
23
|
+
*
|
|
24
|
+
* Reported from the field: a 115-line daemon log containing zero of `reticle_daemon_idle_exit`,
|
|
25
|
+
* `reticle_daemon_close_error`, `daemon_stopped`, `uncaught` or `unhandled` — the daemon simply
|
|
26
|
+
* stopped existing mid-wait. Both crash handlers below already log before exiting, so their silence
|
|
27
|
+
* ruled themselves out and left nothing else to read: no shutdown path had run at all.
|
|
28
|
+
*
|
|
29
|
+
* `exit` fires for every in-process exit including an explicit `process.exit`, and carries the code.
|
|
30
|
+
* The signals cover an external kill. What neither can catch is SIGKILL or an OOM abort — and that
|
|
31
|
+
* is the point: after this, silence in the log is itself the finding, because every other door is
|
|
32
|
+
* now instrumented.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Why the daemon is going away — carried ON the exit line, not left to be inferred.
|
|
36
|
+
*
|
|
37
|
+
* From the field (#123): a real gate log read `reticle_daemon_signalled SIGTERM` then
|
|
38
|
+
* `reticle_daemon_exiting code:0`, then 21 seconds with nothing listening. `code: 0` is the last
|
|
39
|
+
* line before the port goes dark and it cannot distinguish "shut down tidily" from "the bridge every
|
|
40
|
+
* app on this machine needs is now gone". A correct install was written up as a failure naming the
|
|
41
|
+
* fixture because of it.
|
|
42
|
+
*
|
|
43
|
+
* `UNKNOWN` is the load-bearing member, not a fallback: it means the process left through Node
|
|
44
|
+
* WITHOUT passing a shutdown path — an uncaught throw, a stray `process.exit`. That is a different
|
|
45
|
+
* fact from an idle exit, and the one worth noticing.
|
|
46
|
+
*/
|
|
47
|
+
export declare const DaemonExitReason: {
|
|
48
|
+
/** A shutdown signal arrived (SIGTERM/SIGINT). */
|
|
49
|
+
readonly SIGNAL: "signal";
|
|
50
|
+
/** The idle timer fired and the daemon retired itself. */
|
|
51
|
+
readonly IDLE: "idle";
|
|
52
|
+
/** Left through Node, but not via any shutdown path we own. */
|
|
53
|
+
readonly UNKNOWN: "unknown";
|
|
54
|
+
};
|
|
55
|
+
export type DaemonExitReason = (typeof DaemonExitReason)[keyof typeof DaemonExitReason];
|
|
56
|
+
/** Record why the daemon is shutting down. Called by a shutdown path before it exits. */
|
|
57
|
+
export declare function recordExitReason(reason: DaemonExitReason): void;
|
|
58
|
+
export declare function installExitTrace(proc: ProcessLike, log: LogFn): void;
|
|
59
|
+
export declare function installDaemonResilience(proc: ProcessLike, log: LogFn, onFatal: () => void): void;
|
|
60
|
+
/**
|
|
61
|
+
* Process-level resilience for the MCP PROXY, whose rule is the opposite of the daemon's.
|
|
62
|
+
*
|
|
63
|
+
* The daemon exits on an uncaught throw because the next `reticle mcp` respawns it. Nothing respawns
|
|
64
|
+
* the proxy — it is the stdio server the editor launched, and its exit is what the user experiences
|
|
65
|
+
* as "the MCP server disconnected, open /mcp and reconnect". So it logs and keeps serving: its whole
|
|
66
|
+
* state is a socket and a queue, and it already knows how to rebuild both (see the reconnect and
|
|
67
|
+
* dormant paths). A crashed-but-serving proxy answers the handshake and `tools/list` from cache; a
|
|
68
|
+
* dead one answers nothing and needs a human.
|
|
69
|
+
*
|
|
70
|
+
* `onCrash` is for reporting only — it must not end the process.
|
|
71
|
+
*/
|
|
72
|
+
export declare function installProxyResilience(proc: ProcessLike, log: LogFn, onCrash?: (kind: CrashKind, cause: unknown) => void): void;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-level resilience for the long-running daemon. The daemon serves many agents at once, so a
|
|
3
|
+
* single stray error must not take the whole fleet down:
|
|
4
|
+
*
|
|
5
|
+
* - unhandledRejection (a fire-and-forget promise nobody awaited — the common case in async WS/pool
|
|
6
|
+
* code) → LOG and keep running. One agent's async slip-up can't crash the daemon for everyone.
|
|
7
|
+
* - uncaughtException (a synchronous throw escaped all try/catch) → the process state is undefined
|
|
8
|
+
* per Node's guidance, so LOG a clear reason and exit cleanly; the next `reticle mcp` respawns a fresh
|
|
9
|
+
* daemon, which beats crashing silently or limping along corrupt.
|
|
10
|
+
*/
|
|
11
|
+
import { RETICLE_DEFAULT_PORT, TelemetryActor, TelemetryEventKind } from '@reticlehq/core';
|
|
12
|
+
import { errorSkeleton, errorTypeOf, fingerprintCrash, MAX_REPORTED_FRAMES, reticleFrames, } from '../telemetry/error-fingerprint.js';
|
|
13
|
+
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
14
|
+
import { machineSnapshot } from '../telemetry/machine-snapshot.js';
|
|
15
|
+
import { crashCause } from '../telemetry/crash-cause.js';
|
|
16
|
+
import { getTelemetry } from '../telemetry/telemetry.js';
|
|
17
|
+
function describe(value) {
|
|
18
|
+
return value instanceof Error ? value.message : String(value);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Node error codes that mean THE OTHER END WENT AWAY, not "we broke".
|
|
22
|
+
*
|
|
23
|
+
* One real session produced nine `runtime_crashed` events, all `write EPIPE`: the MCP client closed
|
|
24
|
+
* its half of the stdio pipe and the very next `process.stdout.write` failed, which is the ordinary
|
|
25
|
+
* way a client leaves. Counted as uncaught exceptions they poison the only metric that answers "is
|
|
26
|
+
* Reticle stable?" — and they recur every single time an editor is closed.
|
|
27
|
+
*
|
|
28
|
+
* Matched on `err.code`, never on the message: a message is prose, gets wrapped, localised and
|
|
29
|
+
* rewritten, and matching it would eventually swallow a real crash that merely mentioned a pipe.
|
|
30
|
+
*
|
|
31
|
+
* - `EPIPE` — wrote to a pipe whose reader is gone. The stdio case, and the nine.
|
|
32
|
+
* - `ECONNRESET` — the peer reset the socket; stdio is a socket when the parent piped it, and the
|
|
33
|
+
* HTTP/SSE surface produces it when a client vanishes mid-response.
|
|
34
|
+
* - `ERR_STREAM_DESTROYED` — the same race one tick later: the stream was already torn down when
|
|
35
|
+
* the write landed.
|
|
36
|
+
*/
|
|
37
|
+
const DISCONNECT_CODES = new Set([
|
|
38
|
+
'EPIPE',
|
|
39
|
+
'ECONNRESET',
|
|
40
|
+
'ERR_STREAM_DESTROYED',
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* Nobody went away — nobody was ever there. A refused connect is the daemon not being up YET.
|
|
44
|
+
*
|
|
45
|
+
* This is the difference that made `runtime_crashed` useless. Measured 2026-08-10/11: **84 of 84
|
|
46
|
+
* crash events were one fingerprint, `connect ECONNREFUSED`, and every affected user was in the
|
|
47
|
+
* never-verified set.** The proxy is BUILT to tolerate this — it answers the handshake from cache
|
|
48
|
+
* and wakes a daemon on the next request — so reporting it as a crash meant the one metric that is
|
|
49
|
+
* supposed to say "Reticle broke" said nothing but "the daemon had not booted yet", and a real crash
|
|
50
|
+
* would have been invisible underneath it.
|
|
51
|
+
*
|
|
52
|
+
* Absorbed, not silenced: it still logs, and the outage it belongs to is already counted as
|
|
53
|
+
* `connect_error` on `mcp_connection_lost`, which is the metric that can actually act on it.
|
|
54
|
+
*/
|
|
55
|
+
const UNREACHABLE_CODES = new Set(['ECONNREFUSED']);
|
|
56
|
+
/** The expected-failure code this value carries, and which class it belongs to. */
|
|
57
|
+
function expectedCode(value) {
|
|
58
|
+
const raw = value?.code;
|
|
59
|
+
if ('string' !== typeof raw)
|
|
60
|
+
return undefined;
|
|
61
|
+
if (DISCONNECT_CODES.has(raw))
|
|
62
|
+
return { code: raw, unreachable: false };
|
|
63
|
+
if (UNREACHABLE_CODES.has(raw))
|
|
64
|
+
return { code: raw, unreachable: true };
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Log an expected failure and report `true`, so the caller skips the crash report.
|
|
69
|
+
*
|
|
70
|
+
* It is still WRITTEN DOWN. A client leaving is normal, but a daemon that suddenly logs a hundred of
|
|
71
|
+
* these is a finding — the difference between "not a crash" and "not worth knowing" is the whole
|
|
72
|
+
* reason this logs instead of returning early in silence.
|
|
73
|
+
*/
|
|
74
|
+
function absorbExpected(value, log, events) {
|
|
75
|
+
const hit = expectedCode(value);
|
|
76
|
+
if (hit === undefined)
|
|
77
|
+
return false;
|
|
78
|
+
log(hit.unreachable ? events.unreachable : events.disconnected, {
|
|
79
|
+
code: hit.code,
|
|
80
|
+
note: hit.unreachable
|
|
81
|
+
? 'the daemon was not reachable — expected while it boots, not a crash'
|
|
82
|
+
: 'the client went away — expected, not a crash',
|
|
83
|
+
});
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/** The two ways a failure reaches the top of the process. Named so the analytics can tell them apart. */
|
|
87
|
+
export const CrashKind = {
|
|
88
|
+
UNHANDLED_REJECTION: 'unhandled_rejection',
|
|
89
|
+
UNCAUGHT_EXCEPTION: 'uncaught_exception',
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* The ports Reticle itself uses, so a refusal can be classified as ours or somebody else's.
|
|
93
|
+
*
|
|
94
|
+
* Read here rather than threaded through `installCrashHandlers`: a crash handler is installed once,
|
|
95
|
+
* at startup, and the port can be set after it. The values are only ever compared against — the
|
|
96
|
+
* port a crash names is turned into an enum and then discarded.
|
|
97
|
+
*/
|
|
98
|
+
function knownPorts() {
|
|
99
|
+
const configured = Number(process.env['RETICLE_PORT']);
|
|
100
|
+
return Number.isInteger(configured) && configured > 0
|
|
101
|
+
? [RETICLE_DEFAULT_PORT, configured]
|
|
102
|
+
: [RETICLE_DEFAULT_PORT];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Report a crash with enough detail to actually diagnose it.
|
|
106
|
+
*
|
|
107
|
+
* This is the only place in the product that learns about a crash at all. An uncaught exception in a
|
|
108
|
+
* long-running daemon takes down every agent attached to it, and the only other record is a line in a
|
|
109
|
+
* local log nobody sends us.
|
|
110
|
+
*
|
|
111
|
+
* The first version sent a hash and nothing else, which made crashes rankable and undiagnosable — you
|
|
112
|
+
* could see that forty machines hit `a3f2c1d8e9b0` and had no way to learn what that was. So it now
|
|
113
|
+
* carries the four things an RCA needs: WHERE (our own frames, with function names and line numbers),
|
|
114
|
+
* WHAT (the message with every variable part stripped), WHEN (the tool in flight), and WHY-ish (the
|
|
115
|
+
* agent's approach run before it). All of it is our code and our vocabulary; the user's stack frames,
|
|
116
|
+
* their paths, and the contents of their message are removed before any of it is built.
|
|
117
|
+
*
|
|
118
|
+
* Best-effort and wrapped: reporting a crash must never be the reason a crash gets worse.
|
|
119
|
+
*/
|
|
120
|
+
function reportCrash(kind, value) {
|
|
121
|
+
try {
|
|
122
|
+
const errorType = errorTypeOf(value);
|
|
123
|
+
const stack = value instanceof Error ? (value.stack ?? '') : '';
|
|
124
|
+
const message = describe(value);
|
|
125
|
+
const { breadcrumb, inFlight } = getSessionMetrics().trail;
|
|
126
|
+
const machine = machineSnapshot();
|
|
127
|
+
// Where the frames cannot say. A refused socket's stack is entirely node internals, so
|
|
128
|
+
// `reticleFrames` correctly keeps nothing and the report lands with no location at all — the
|
|
129
|
+
// single commonest crash shape in a loopback-heavy tool.
|
|
130
|
+
const cause = crashCause(value, knownPorts());
|
|
131
|
+
void getTelemetry().emit(TelemetryEventKind.RUNTIME_CRASHED, {
|
|
132
|
+
// A crash is ALWAYS reached through something the agent asked for — the daemon does nothing on
|
|
133
|
+
// its own — so attributing it to the agent is accurate rather than a guess.
|
|
134
|
+
actor: TelemetryActor.AGENT,
|
|
135
|
+
crash: {
|
|
136
|
+
kind,
|
|
137
|
+
errorType,
|
|
138
|
+
fingerprint: fingerprintCrash(errorType, stack, message),
|
|
139
|
+
// The skeleton, not the message: every quoted string, path, URL, id and number is replaced
|
|
140
|
+
// by `*` first. This is what makes the fingerprint mean something to a human reading a chart.
|
|
141
|
+
message: errorSkeleton(message).slice(0, 300),
|
|
142
|
+
// Our own frames, our own published code. The file, the line, and the function — which is the
|
|
143
|
+
// whole of "where did it break". Frames from the user's app never survive this.
|
|
144
|
+
frames: reticleFrames(stack).slice(0, MAX_REPORTED_FRAMES),
|
|
145
|
+
...(inFlight !== undefined ? { tool: inFlight } : {}),
|
|
146
|
+
...(breadcrumb.length > 0 ? { breadcrumb } : {}),
|
|
147
|
+
// Crashes cluster hard by runtime version and architecture, and we were recording neither.
|
|
148
|
+
nodeVersion: process.versions.node,
|
|
149
|
+
arch: process.arch,
|
|
150
|
+
// "Out of memory" and "our bug" look identical in a stack trace. This is what tells them apart.
|
|
151
|
+
...(machine !== undefined ? { machine } : {}),
|
|
152
|
+
// The syscall, the errno, whether it was loopback, whether the port was ours, and the
|
|
153
|
+
// innermost frame in NODE's own source. Present only when the error carries them.
|
|
154
|
+
...cause,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
/* a crash report must never be the reason a crash gets worse */
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Log EVERY way this process can leave, so a death always leaves a line.
|
|
164
|
+
*
|
|
165
|
+
* Reported from the field: a 115-line daemon log containing zero of `reticle_daemon_idle_exit`,
|
|
166
|
+
* `reticle_daemon_close_error`, `daemon_stopped`, `uncaught` or `unhandled` — the daemon simply
|
|
167
|
+
* stopped existing mid-wait. Both crash handlers below already log before exiting, so their silence
|
|
168
|
+
* ruled themselves out and left nothing else to read: no shutdown path had run at all.
|
|
169
|
+
*
|
|
170
|
+
* `exit` fires for every in-process exit including an explicit `process.exit`, and carries the code.
|
|
171
|
+
* The signals cover an external kill. What neither can catch is SIGKILL or an OOM abort — and that
|
|
172
|
+
* is the point: after this, silence in the log is itself the finding, because every other door is
|
|
173
|
+
* now instrumented.
|
|
174
|
+
*/
|
|
175
|
+
/**
|
|
176
|
+
* Why the daemon is going away — carried ON the exit line, not left to be inferred.
|
|
177
|
+
*
|
|
178
|
+
* From the field (#123): a real gate log read `reticle_daemon_signalled SIGTERM` then
|
|
179
|
+
* `reticle_daemon_exiting code:0`, then 21 seconds with nothing listening. `code: 0` is the last
|
|
180
|
+
* line before the port goes dark and it cannot distinguish "shut down tidily" from "the bridge every
|
|
181
|
+
* app on this machine needs is now gone". A correct install was written up as a failure naming the
|
|
182
|
+
* fixture because of it.
|
|
183
|
+
*
|
|
184
|
+
* `UNKNOWN` is the load-bearing member, not a fallback: it means the process left through Node
|
|
185
|
+
* WITHOUT passing a shutdown path — an uncaught throw, a stray `process.exit`. That is a different
|
|
186
|
+
* fact from an idle exit, and the one worth noticing.
|
|
187
|
+
*/
|
|
188
|
+
export const DaemonExitReason = {
|
|
189
|
+
/** A shutdown signal arrived (SIGTERM/SIGINT). */
|
|
190
|
+
SIGNAL: 'signal',
|
|
191
|
+
/** The idle timer fired and the daemon retired itself. */
|
|
192
|
+
IDLE: 'idle',
|
|
193
|
+
/** Left through Node, but not via any shutdown path we own. */
|
|
194
|
+
UNKNOWN: 'unknown',
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Set by whichever shutdown path is running, read once by the exit handler.
|
|
198
|
+
*
|
|
199
|
+
* A module-level value rather than a parameter because the two are separated by the whole process
|
|
200
|
+
* lifetime: the reason is known when shutdown STARTS and the exit line is written when Node is on
|
|
201
|
+
* its way out. Exit runs once, on one thread, after everything else — so there is nothing to race.
|
|
202
|
+
*/
|
|
203
|
+
let exitReason = DaemonExitReason.UNKNOWN;
|
|
204
|
+
/** Record why the daemon is shutting down. Called by a shutdown path before it exits. */
|
|
205
|
+
export function recordExitReason(reason) {
|
|
206
|
+
exitReason = reason;
|
|
207
|
+
}
|
|
208
|
+
export function installExitTrace(proc, log) {
|
|
209
|
+
proc.on('exit', (code) => {
|
|
210
|
+
log('reticle_daemon_exiting', {
|
|
211
|
+
code: 'number' === typeof code ? code : null,
|
|
212
|
+
reason: exitReason,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) {
|
|
216
|
+
proc.on(signal, () => {
|
|
217
|
+
// Log only. Whatever installed a real handler for this signal still runs; adding an exit here
|
|
218
|
+
// would race the shutdown that flushes the session summary.
|
|
219
|
+
log('reticle_daemon_signalled', { signal });
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export function installDaemonResilience(proc, log, onFatal) {
|
|
224
|
+
installExitTrace(proc, log);
|
|
225
|
+
const expected = {
|
|
226
|
+
disconnected: 'reticle_daemon_client_disconnected',
|
|
227
|
+
unreachable: 'reticle_daemon_peer_unreachable',
|
|
228
|
+
};
|
|
229
|
+
proc.on('unhandledRejection', (reason) => {
|
|
230
|
+
if (absorbExpected(reason, log, expected))
|
|
231
|
+
return;
|
|
232
|
+
log('reticle_daemon_unhandled_rejection', { reason: describe(reason) });
|
|
233
|
+
reportCrash(CrashKind.UNHANDLED_REJECTION, reason);
|
|
234
|
+
});
|
|
235
|
+
proc.on('uncaughtException', (err) => {
|
|
236
|
+
// Not fatal either: Node's "the process state is undefined" guidance is about a throw that
|
|
237
|
+
// escaped everything, not about a write to a socket somebody closed. Exiting here would let one
|
|
238
|
+
// departing client take down the daemon serving every other agent.
|
|
239
|
+
if (absorbExpected(err, log, expected))
|
|
240
|
+
return;
|
|
241
|
+
log('reticle_daemon_uncaught_exception', { error: describe(err) });
|
|
242
|
+
reportCrash(CrashKind.UNCAUGHT_EXCEPTION, err);
|
|
243
|
+
onFatal();
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Process-level resilience for the MCP PROXY, whose rule is the opposite of the daemon's.
|
|
248
|
+
*
|
|
249
|
+
* The daemon exits on an uncaught throw because the next `reticle mcp` respawns it. Nothing respawns
|
|
250
|
+
* the proxy — it is the stdio server the editor launched, and its exit is what the user experiences
|
|
251
|
+
* as "the MCP server disconnected, open /mcp and reconnect". So it logs and keeps serving: its whole
|
|
252
|
+
* state is a socket and a queue, and it already knows how to rebuild both (see the reconnect and
|
|
253
|
+
* dormant paths). A crashed-but-serving proxy answers the handshake and `tools/list` from cache; a
|
|
254
|
+
* dead one answers nothing and needs a human.
|
|
255
|
+
*
|
|
256
|
+
* `onCrash` is for reporting only — it must not end the process.
|
|
257
|
+
*/
|
|
258
|
+
export function installProxyResilience(proc, log, onCrash = reportCrash) {
|
|
259
|
+
const expected = {
|
|
260
|
+
disconnected: 'reticle_mcp_proxy_client_disconnected',
|
|
261
|
+
unreachable: 'reticle_mcp_proxy_daemon_unreachable',
|
|
262
|
+
};
|
|
263
|
+
proc.on('unhandledRejection', (reason) => {
|
|
264
|
+
if (absorbExpected(reason, log, expected))
|
|
265
|
+
return;
|
|
266
|
+
log('reticle_mcp_proxy_unhandled_rejection', { reason: describe(reason) });
|
|
267
|
+
onCrash(CrashKind.UNHANDLED_REJECTION, reason);
|
|
268
|
+
});
|
|
269
|
+
proc.on('uncaughtException', (err) => {
|
|
270
|
+
// The client closing its end of stdio is how this process is SUPPOSED to end its day.
|
|
271
|
+
if (absorbExpected(err, log, expected))
|
|
272
|
+
return;
|
|
273
|
+
log('reticle_mcp_proxy_uncaught_exception', {
|
|
274
|
+
error: describe(err),
|
|
275
|
+
note: 'still serving — exiting here would disconnect the MCP client',
|
|
276
|
+
});
|
|
277
|
+
onCrash(CrashKind.UNCAUGHT_EXCEPTION, err);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this daemon has ever been useful to anyone.
|
|
3
|
+
*
|
|
4
|
+
* Reticle is usually registered as a GLOBAL MCP server, so every agent session in every directory
|
|
5
|
+
* spawns it — including the three quarters of directories that are not web apps at all. Measured
|
|
6
|
+
* over a day: `stack` was null for 92 of 121 profiled projects, 50 were not even git repositories,
|
|
7
|
+
* and 74% of daemon sessions never called a single tool. Median daemon lifetime 28 minutes; total
|
|
8
|
+
* uptime across those sessions 10,816 minutes against 4.6 minutes of actual work — a duty cycle of
|
|
9
|
+
* **0.04%**.
|
|
10
|
+
*
|
|
11
|
+
* The existing idle-shutdown could never fire on any of it, because its predicate is
|
|
12
|
+
* `!agentConnected && …` and an attached agent keeps `agentConnected` true for the whole editor
|
|
13
|
+
* session. So a daemon sat for half an hour, in a directory with no app, doing nothing, holding
|
|
14
|
+
* memory and a port.
|
|
15
|
+
*
|
|
16
|
+
* The rule here is deliberately the most conservative one that still catches that population:
|
|
17
|
+
* a daemon is USELESS only if it has never served a tool call AND no browser has ever connected to
|
|
18
|
+
* it AND nothing is leased from the pool. All three are lifetime facts, not windows — so nothing
|
|
19
|
+
* transient trips it, and a daemon in this state provably holds no recording, baseline or session
|
|
20
|
+
* anybody could lose. When it does shut down, the MCP proxy reconnects and `ensureDaemon` spawns a
|
|
21
|
+
* fresh one on the next call, which is a path already exercised hundreds of times a day.
|
|
22
|
+
*/
|
|
23
|
+
export declare function noteToolCall(): void;
|
|
24
|
+
export declare function everServedToolCall(): boolean;
|
|
25
|
+
/** Tests only. */
|
|
26
|
+
export declare function resetDaemonUsefulness(): void;
|
|
27
|
+
interface UsefulnessFacts {
|
|
28
|
+
/** Has any tool been dispatched since this daemon booted? */
|
|
29
|
+
servedToolCall: boolean;
|
|
30
|
+
/** Has any browser session ever connected? */
|
|
31
|
+
everConnected: boolean;
|
|
32
|
+
/** Active pool leases — a lease means somebody is mid-flight even with no session yet. */
|
|
33
|
+
activeLeases: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* True when the daemon has done nothing for anyone and has nothing to lose by exiting.
|
|
37
|
+
*
|
|
38
|
+
* Every clause is a LIFETIME fact. "No tool call in the last five minutes" would fire on a thinking
|
|
39
|
+
* pause; "no tool call ever" cannot.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isUselessDaemon(facts: UsefulnessFacts): boolean;
|
|
42
|
+
/** The little of SessionManager the idle predicate reads — structural, so a fake needs two methods. */
|
|
43
|
+
interface IdleSessions {
|
|
44
|
+
count(): number;
|
|
45
|
+
everConnected(): boolean;
|
|
46
|
+
}
|
|
47
|
+
/** The little of BrowserPool it reads. */
|
|
48
|
+
interface IdlePool {
|
|
49
|
+
activeCount(): number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The daemon's whole "is anything happening" question.
|
|
53
|
+
*
|
|
54
|
+
* Two ways to be idle. The original — nobody attached, nothing connected. And the one that was
|
|
55
|
+
* missing: an agent IS attached but this daemon has never served a tool call and no browser has ever
|
|
56
|
+
* connected to it. `agentConnected` stays true for an entire editor session, so without the second
|
|
57
|
+
* clause the shutdown watcher could never fire on the 74% of daemons that spawn in a directory with
|
|
58
|
+
* no app and sit there for half an hour.
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildIdlePredicate(agentConnected: () => boolean, sessions: IdleSessions, pool: IdlePool): () => boolean;
|
|
61
|
+
export {};
|