@reticlehq/server 2.4.0 → 2.5.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 +702 -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} +1 -1
- package/dist/{bridge.js → bridge/bridge.js} +63 -13
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +1 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +9 -9
- 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-doctor.js → cli/cli-doctor.js} +9 -2
- 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-parse.d.ts → cli/cli-parse.d.ts} +2 -11
- package/dist/{cli-parse.js → cli/cli-parse.js} +113 -60
- 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.d.ts +2 -2
- package/dist/cli.js +174 -92
- 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 +49 -0
- package/dist/daemon/daemon-resilience.js +191 -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} +80 -48
- 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} +13 -0
- package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
- 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 +43 -20
- package/dist/events/predicate-eval.js +159 -55
- package/dist/events/predicate.d.ts +2 -2
- package/dist/events/predicate.js +112 -35
- package/dist/events/reconcile.js +5 -5
- package/dist/events/ring-buffer.js +1 -1
- 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 +5 -7
- package/dist/flows/flow-replay.js +114 -47
- 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 +157 -6
- package/dist/flows/flow-success.js +20 -15
- package/dist/flows/flow-tools.js +53 -7
- 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/server-verify.d.ts +1 -1
- package/dist/flows/server-verify.js +5 -5
- 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 +16 -3
- package/dist/honesty/blind-spots.js +24 -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 +22 -3
- package/dist/honesty/verified.js +48 -8
- package/dist/http-server.js +14 -7
- package/dist/hunt/hunt-report.d.ts +2 -1
- package/dist/hunt/hunt-report.js +2 -2
- package/dist/index.d.ts +8 -6
- package/dist/index.js +32 -25
- package/dist/init/agent-rules.d.ts +2 -1
- package/dist/init/agent-rules.js +45 -7
- 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 +37 -0
- package/dist/init/cra.js +90 -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 +3 -2
- package/dist/init/detect.js +16 -5
- 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 +23 -6
- package/dist/init/plan-framework.d.ts +16 -1
- package/dist/init/plan-framework.js +132 -8
- package/dist/init/plan.d.ts +73 -8
- package/dist/init/plan.js +133 -34
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +5 -0
- package/dist/init/run.js +210 -36
- 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/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 +31 -0
- package/dist/mcp/mcp-outage.js +48 -0
- package/dist/mcp/mcp-proxy.d.ts +124 -0
- package/dist/mcp/mcp-proxy.js +697 -0
- package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
- package/dist/{mcp.js → mcp/mcp.js} +110 -35
- 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/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/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 +38 -0
- package/dist/session/no-session-diagnosis.js +70 -0
- package/dist/session/no-session-watch.d.ts +19 -0
- package/dist/session/no-session-watch.js +61 -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/session-health.d.ts +8 -1
- package/dist/session/session-health.js +8 -7
- package/dist/session/session-info.d.ts +26 -7
- package/dist/session/session-info.js +42 -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 +3 -0
- package/dist/session/session-manager.js +52 -9
- 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 +14 -2
- package/dist/session/session.js +69 -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/daemon-telemetry.d.ts +19 -0
- package/dist/telemetry/daemon-telemetry.js +12 -2
- package/dist/telemetry/dev-repo.js +4 -4
- package/dist/telemetry/error-fingerprint.js +21 -2
- package/dist/telemetry/feedback-cli.d.ts +1 -1
- package/dist/telemetry/feedback-cli.js +4 -4
- package/dist/telemetry/feedback-context.d.ts +22 -0
- package/dist/telemetry/feedback-context.js +41 -1
- 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.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +2 -0
- package/dist/telemetry/session-metrics.js +68 -6
- package/dist/telemetry/telemetry.d.ts +27 -2
- package/dist/telemetry/telemetry.js +119 -29
- 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 +84 -115
- 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 +3 -1
- package/dist/tools/assert-verdict.js +8 -5
- package/dist/tools/browser-tools.js +30 -11
- package/dist/tools/contract-tools.js +1 -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 +90 -18
- package/dist/tools/error-recovery.d.ts +20 -2
- package/dist/tools/error-recovery.js +179 -4
- 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 -6
- package/dist/tools/invoke-tool.js +57 -32
- 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-result.d.ts +17 -0
- package/dist/tools/navigate-result.js +31 -0
- package/dist/tools/observe-tools.js +54 -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 +88 -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 +8 -0
- package/dist/tools/tool-surface.d.ts +83 -0
- package/dist/tools/tool-surface.js +204 -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/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.md +28 -0
- package/docs/flows.md +166 -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/mint.json +61 -0
- package/docs/multi-agent-testing.md +55 -0
- package/docs/platform-integration.md +122 -0
- package/docs/telemetry-contract.md +214 -0
- package/docs/telemetry.md +184 -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-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-doctor.d.ts → cli/cli-doctor.d.ts} +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,49 @@
|
|
|
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
|
+
export declare function installExitTrace(proc: ProcessLike, log: LogFn): void;
|
|
35
|
+
export declare function installDaemonResilience(proc: ProcessLike, log: LogFn, onFatal: () => void): void;
|
|
36
|
+
/**
|
|
37
|
+
* Process-level resilience for the MCP PROXY, whose rule is the opposite of the daemon's.
|
|
38
|
+
*
|
|
39
|
+
* The daemon exits on an uncaught throw because the next `reticle mcp` respawns it. Nothing respawns
|
|
40
|
+
* the proxy — it is the stdio server the editor launched, and its exit is what the user experiences
|
|
41
|
+
* as "the MCP server disconnected, open /mcp and reconnect". So it logs and keeps serving: its whole
|
|
42
|
+
* state is a socket and a queue, and it already knows how to rebuild both (see the reconnect and
|
|
43
|
+
* dormant paths). A crashed-but-serving proxy answers the handshake and `tools/list` from cache; a
|
|
44
|
+
* dead one answers nothing and needs a human.
|
|
45
|
+
*
|
|
46
|
+
* `onCrash` is for reporting only — it must not end the process.
|
|
47
|
+
*/
|
|
48
|
+
export declare function installProxyResilience(proc: ProcessLike, log: LogFn, onCrash?: (kind: CrashKind, cause: unknown) => void): void;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,191 @@
|
|
|
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 { 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 { getTelemetry } from '../telemetry/telemetry.js';
|
|
16
|
+
function describe(value) {
|
|
17
|
+
return value instanceof Error ? value.message : String(value);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Node error codes that mean THE OTHER END WENT AWAY, not "we broke".
|
|
21
|
+
*
|
|
22
|
+
* One real session produced nine `runtime_crashed` events, all `write EPIPE`: the MCP client closed
|
|
23
|
+
* its half of the stdio pipe and the very next `process.stdout.write` failed, which is the ordinary
|
|
24
|
+
* way a client leaves. Counted as uncaught exceptions they poison the only metric that answers "is
|
|
25
|
+
* Reticle stable?" — and they recur every single time an editor is closed.
|
|
26
|
+
*
|
|
27
|
+
* Matched on `err.code`, never on the message: a message is prose, gets wrapped, localised and
|
|
28
|
+
* rewritten, and matching it would eventually swallow a real crash that merely mentioned a pipe.
|
|
29
|
+
*
|
|
30
|
+
* - `EPIPE` — wrote to a pipe whose reader is gone. The stdio case, and the nine.
|
|
31
|
+
* - `ECONNRESET` — the peer reset the socket; stdio is a socket when the parent piped it, and the
|
|
32
|
+
* HTTP/SSE surface produces it when a client vanishes mid-response.
|
|
33
|
+
* - `ERR_STREAM_DESTROYED` — the same race one tick later: the stream was already torn down when
|
|
34
|
+
* the write landed.
|
|
35
|
+
*/
|
|
36
|
+
const DISCONNECT_CODES = new Set([
|
|
37
|
+
'EPIPE',
|
|
38
|
+
'ECONNRESET',
|
|
39
|
+
'ERR_STREAM_DESTROYED',
|
|
40
|
+
]);
|
|
41
|
+
/** The disconnect code this failure carries, if that is all it was. */
|
|
42
|
+
function disconnectCode(value) {
|
|
43
|
+
const code = value?.code;
|
|
44
|
+
return 'string' === typeof code && DISCONNECT_CODES.has(code) ? code : undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Log an expected disconnect and report `true`, so the caller skips the crash report.
|
|
48
|
+
*
|
|
49
|
+
* It is still WRITTEN DOWN. A client leaving is normal, but a daemon that suddenly logs a hundred of
|
|
50
|
+
* these is a finding — the difference between "not a crash" and "not worth knowing" is the whole
|
|
51
|
+
* reason this logs instead of returning early in silence.
|
|
52
|
+
*/
|
|
53
|
+
function absorbDisconnect(value, log, event) {
|
|
54
|
+
const code = disconnectCode(value);
|
|
55
|
+
if (code === undefined)
|
|
56
|
+
return false;
|
|
57
|
+
log(event, { code, note: 'the client went away — expected, not a crash' });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/** The two ways a failure reaches the top of the process. Named so the analytics can tell them apart. */
|
|
61
|
+
export const CrashKind = {
|
|
62
|
+
UNHANDLED_REJECTION: 'unhandled_rejection',
|
|
63
|
+
UNCAUGHT_EXCEPTION: 'uncaught_exception',
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Report a crash with enough detail to actually diagnose it.
|
|
67
|
+
*
|
|
68
|
+
* This is the only place in the product that learns about a crash at all. An uncaught exception in a
|
|
69
|
+
* long-running daemon takes down every agent attached to it, and the only other record is a line in a
|
|
70
|
+
* local log nobody sends us.
|
|
71
|
+
*
|
|
72
|
+
* The first version sent a hash and nothing else, which made crashes rankable and undiagnosable — you
|
|
73
|
+
* could see that forty machines hit `a3f2c1d8e9b0` and had no way to learn what that was. So it now
|
|
74
|
+
* carries the four things an RCA needs: WHERE (our own frames, with function names and line numbers),
|
|
75
|
+
* WHAT (the message with every variable part stripped), WHEN (the tool in flight), and WHY-ish (the
|
|
76
|
+
* agent's approach run before it). All of it is our code and our vocabulary; the user's stack frames,
|
|
77
|
+
* their paths, and the contents of their message are removed before any of it is built.
|
|
78
|
+
*
|
|
79
|
+
* Best-effort and wrapped: reporting a crash must never be the reason a crash gets worse.
|
|
80
|
+
*/
|
|
81
|
+
function reportCrash(kind, value) {
|
|
82
|
+
try {
|
|
83
|
+
const errorType = errorTypeOf(value);
|
|
84
|
+
const stack = value instanceof Error ? (value.stack ?? '') : '';
|
|
85
|
+
const message = describe(value);
|
|
86
|
+
const { breadcrumb, inFlight } = getSessionMetrics().trail;
|
|
87
|
+
const machine = machineSnapshot();
|
|
88
|
+
void getTelemetry().emit(TelemetryEventKind.RUNTIME_CRASHED, {
|
|
89
|
+
// A crash is ALWAYS reached through something the agent asked for — the daemon does nothing on
|
|
90
|
+
// its own — so attributing it to the agent is accurate rather than a guess.
|
|
91
|
+
actor: TelemetryActor.AGENT,
|
|
92
|
+
crash: {
|
|
93
|
+
kind,
|
|
94
|
+
errorType,
|
|
95
|
+
fingerprint: fingerprintCrash(errorType, stack, message),
|
|
96
|
+
// The skeleton, not the message: every quoted string, path, URL, id and number is replaced
|
|
97
|
+
// by `*` first. This is what makes the fingerprint mean something to a human reading a chart.
|
|
98
|
+
message: errorSkeleton(message).slice(0, 300),
|
|
99
|
+
// Our own frames, our own published code. The file, the line, and the function — which is the
|
|
100
|
+
// whole of "where did it break". Frames from the user's app never survive this.
|
|
101
|
+
frames: reticleFrames(stack).slice(0, MAX_REPORTED_FRAMES),
|
|
102
|
+
...(inFlight !== undefined ? { tool: inFlight } : {}),
|
|
103
|
+
...(breadcrumb.length > 0 ? { breadcrumb } : {}),
|
|
104
|
+
// Crashes cluster hard by runtime version and architecture, and we were recording neither.
|
|
105
|
+
nodeVersion: process.versions.node,
|
|
106
|
+
arch: process.arch,
|
|
107
|
+
// "Out of memory" and "our bug" look identical in a stack trace. This is what tells them apart.
|
|
108
|
+
...(machine !== undefined ? { machine } : {}),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* a crash report must never be the reason a crash gets worse */
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Log EVERY way this process can leave, so a death always leaves a line.
|
|
118
|
+
*
|
|
119
|
+
* Reported from the field: a 115-line daemon log containing zero of `reticle_daemon_idle_exit`,
|
|
120
|
+
* `reticle_daemon_close_error`, `daemon_stopped`, `uncaught` or `unhandled` — the daemon simply
|
|
121
|
+
* stopped existing mid-wait. Both crash handlers below already log before exiting, so their silence
|
|
122
|
+
* ruled themselves out and left nothing else to read: no shutdown path had run at all.
|
|
123
|
+
*
|
|
124
|
+
* `exit` fires for every in-process exit including an explicit `process.exit`, and carries the code.
|
|
125
|
+
* The signals cover an external kill. What neither can catch is SIGKILL or an OOM abort — and that
|
|
126
|
+
* is the point: after this, silence in the log is itself the finding, because every other door is
|
|
127
|
+
* now instrumented.
|
|
128
|
+
*/
|
|
129
|
+
export function installExitTrace(proc, log) {
|
|
130
|
+
proc.on('exit', (code) => {
|
|
131
|
+
log('reticle_daemon_exiting', { code: 'number' === typeof code ? code : null });
|
|
132
|
+
});
|
|
133
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) {
|
|
134
|
+
proc.on(signal, () => {
|
|
135
|
+
// Log only. Whatever installed a real handler for this signal still runs; adding an exit here
|
|
136
|
+
// would race the shutdown that flushes the session summary.
|
|
137
|
+
log('reticle_daemon_signalled', { signal });
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function installDaemonResilience(proc, log, onFatal) {
|
|
142
|
+
installExitTrace(proc, log);
|
|
143
|
+
const disconnected = 'reticle_daemon_client_disconnected';
|
|
144
|
+
proc.on('unhandledRejection', (reason) => {
|
|
145
|
+
if (absorbDisconnect(reason, log, disconnected))
|
|
146
|
+
return;
|
|
147
|
+
log('reticle_daemon_unhandled_rejection', { reason: describe(reason) });
|
|
148
|
+
reportCrash(CrashKind.UNHANDLED_REJECTION, reason);
|
|
149
|
+
});
|
|
150
|
+
proc.on('uncaughtException', (err) => {
|
|
151
|
+
// Not fatal either: Node's "the process state is undefined" guidance is about a throw that
|
|
152
|
+
// escaped everything, not about a write to a socket somebody closed. Exiting here would let one
|
|
153
|
+
// departing client take down the daemon serving every other agent.
|
|
154
|
+
if (absorbDisconnect(err, log, disconnected))
|
|
155
|
+
return;
|
|
156
|
+
log('reticle_daemon_uncaught_exception', { error: describe(err) });
|
|
157
|
+
reportCrash(CrashKind.UNCAUGHT_EXCEPTION, err);
|
|
158
|
+
onFatal();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Process-level resilience for the MCP PROXY, whose rule is the opposite of the daemon's.
|
|
163
|
+
*
|
|
164
|
+
* The daemon exits on an uncaught throw because the next `reticle mcp` respawns it. Nothing respawns
|
|
165
|
+
* the proxy — it is the stdio server the editor launched, and its exit is what the user experiences
|
|
166
|
+
* as "the MCP server disconnected, open /mcp and reconnect". So it logs and keeps serving: its whole
|
|
167
|
+
* state is a socket and a queue, and it already knows how to rebuild both (see the reconnect and
|
|
168
|
+
* dormant paths). A crashed-but-serving proxy answers the handshake and `tools/list` from cache; a
|
|
169
|
+
* dead one answers nothing and needs a human.
|
|
170
|
+
*
|
|
171
|
+
* `onCrash` is for reporting only — it must not end the process.
|
|
172
|
+
*/
|
|
173
|
+
export function installProxyResilience(proc, log, onCrash = reportCrash) {
|
|
174
|
+
const disconnected = 'reticle_mcp_proxy_client_disconnected';
|
|
175
|
+
proc.on('unhandledRejection', (reason) => {
|
|
176
|
+
if (absorbDisconnect(reason, log, disconnected))
|
|
177
|
+
return;
|
|
178
|
+
log('reticle_mcp_proxy_unhandled_rejection', { reason: describe(reason) });
|
|
179
|
+
onCrash(CrashKind.UNHANDLED_REJECTION, reason);
|
|
180
|
+
});
|
|
181
|
+
proc.on('uncaughtException', (err) => {
|
|
182
|
+
// The client closing its end of stdio is how this process is SUPPOSED to end its day.
|
|
183
|
+
if (absorbDisconnect(err, log, disconnected))
|
|
184
|
+
return;
|
|
185
|
+
log('reticle_mcp_proxy_uncaught_exception', {
|
|
186
|
+
error: describe(err),
|
|
187
|
+
note: 'still serving — exiting here would disconnect the MCP client',
|
|
188
|
+
});
|
|
189
|
+
onCrash(CrashKind.UNCAUGHT_EXCEPTION, err);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -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 {};
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
/** Set the first time any tool is dispatched through runTool — the chokepoint every path shares. */
|
|
24
|
+
let servedToolCall = false;
|
|
25
|
+
export function noteToolCall() {
|
|
26
|
+
servedToolCall = true;
|
|
27
|
+
}
|
|
28
|
+
export function everServedToolCall() {
|
|
29
|
+
return servedToolCall;
|
|
30
|
+
}
|
|
31
|
+
/** Tests only. */
|
|
32
|
+
export function resetDaemonUsefulness() {
|
|
33
|
+
servedToolCall = false;
|
|
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 function isUselessDaemon(facts) {
|
|
42
|
+
return !facts.servedToolCall && !facts.everConnected && 0 === facts.activeLeases;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The daemon's whole "is anything happening" question.
|
|
46
|
+
*
|
|
47
|
+
* Two ways to be idle. The original — nobody attached, nothing connected. And the one that was
|
|
48
|
+
* missing: an agent IS attached but this daemon has never served a tool call and no browser has ever
|
|
49
|
+
* connected to it. `agentConnected` stays true for an entire editor session, so without the second
|
|
50
|
+
* clause the shutdown watcher could never fire on the 74% of daemons that spawn in a directory with
|
|
51
|
+
* no app and sit there for half an hour.
|
|
52
|
+
*/
|
|
53
|
+
export function buildIdlePredicate(agentConnected, sessions, pool) {
|
|
54
|
+
return () => (!agentConnected() ||
|
|
55
|
+
isUselessDaemon({
|
|
56
|
+
servedToolCall: everServedToolCall(),
|
|
57
|
+
everConnected: sessions.everConnected(),
|
|
58
|
+
activeLeases: pool.activeCount(),
|
|
59
|
+
})) &&
|
|
60
|
+
0 === sessions.count() &&
|
|
61
|
+
0 === pool.activeCount();
|
|
62
|
+
}
|
|
@@ -1,4 +1,34 @@
|
|
|
1
|
+
/** Env override for the whole state directory — see ReticleEnv.STATE_DIR. */
|
|
2
|
+
export declare const STATE_DIR_ENV: "RETICLE_STATE_DIR";
|
|
3
|
+
/**
|
|
4
|
+
* Where the daemon keeps pidfiles, the discovery registry and its logs.
|
|
5
|
+
*
|
|
6
|
+
* `~/.reticle` unless overridden. The override exists because a read-only $HOME is not hypothetical
|
|
7
|
+
* — a sandboxed agent or a locked-down Windows profile cannot write there, and until now that meant
|
|
8
|
+
* the daemon could not start at all. Read per call rather than captured at import, so a test (and a
|
|
9
|
+
* wrapper that sets it late) sees the current value.
|
|
10
|
+
*/
|
|
11
|
+
export declare function reticleStateHome(): string;
|
|
1
12
|
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
|
+
/**
|
|
24
|
+
* Roll the log over if it has grown past the cap. Best-effort: a daemon must still start when the
|
|
25
|
+
* log cannot be rotated, because failing to launch over housekeeping is strictly worse than a large
|
|
26
|
+
* file.
|
|
27
|
+
*/
|
|
28
|
+
export declare function rotateDaemonLog(path: string, deps: {
|
|
29
|
+
fileSize(p: string): number;
|
|
30
|
+
renameFile(from: string, to: string): void;
|
|
31
|
+
}, max?: number): boolean;
|
|
2
32
|
export declare function readPid(port: number, home?: string): number | null;
|
|
3
33
|
export declare function isAlive(pid: number): boolean;
|
|
4
34
|
export declare function writePid(port: number): void;
|
|
@@ -20,13 +50,6 @@ export declare function writeDaemonRegistry(port: number, meta: {
|
|
|
20
50
|
projectId?: string;
|
|
21
51
|
startedAt: number;
|
|
22
52
|
}): void;
|
|
23
|
-
export declare function removeDaemonRegistry(port: number): void;
|
|
24
|
-
/**
|
|
25
|
-
* Discover the port of a live daemon serving `projectId` by reading the registry entries in ~/.reticle.
|
|
26
|
-
* Returns null when none matches (caller falls back to the default port — never guesses a mismatched
|
|
27
|
-
* daemon). Stale entries (crashed daemons) are ignored via the pid liveness probe.
|
|
28
|
-
*/
|
|
29
|
-
export declare function discoverDaemonPortForProject(projectId: string | undefined): number | null;
|
|
30
53
|
export declare function isRunning(port: number): boolean;
|
|
31
54
|
/**
|
|
32
55
|
* Find the port of a live reticle daemon by scanning ~/.reticle/daemon-<port>.pid files — so `reticle open`
|
|
@@ -52,13 +75,15 @@ export interface SpawnDaemonDeps {
|
|
|
52
75
|
readonly home: string;
|
|
53
76
|
openFile(path: string, flags: string): number;
|
|
54
77
|
closeFile(fd: number): void;
|
|
78
|
+
/** Size in bytes, or 0 when the file does not exist yet. */
|
|
79
|
+
fileSize(path: string): number;
|
|
80
|
+
renameFile(from: string, to: string): void;
|
|
55
81
|
spawnChild(command: string, args: readonly string[], options: {
|
|
56
82
|
detached: boolean;
|
|
57
83
|
stdio: readonly ('ignore' | number)[];
|
|
58
84
|
}): SpawnedChild;
|
|
59
85
|
pidAlive(pid: number): boolean;
|
|
60
86
|
}
|
|
61
|
-
export declare function defaultSpawnDaemonDeps(): SpawnDaemonDeps;
|
|
62
87
|
/**
|
|
63
88
|
* Spawn the reticle daemon as a detached background process, redirecting output to the log file.
|
|
64
89
|
* Writes the PID file from the parent before returning so callers can call isRunning
|