@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
|
@@ -1,19 +1,61 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, openSync, closeSync, readdirSync, } from 'node:fs';
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, openSync, closeSync, readdirSync, statSync, renameSync, } from 'node:fs';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { daemonRegistryFileName,
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { daemonRegistryFileName, ReticleEnv } from '@reticlehq/core';
|
|
6
|
+
import { log } from '../log.js';
|
|
7
|
+
/** Env override for the whole state directory — see ReticleEnv.STATE_DIR. */
|
|
8
|
+
export const STATE_DIR_ENV = ReticleEnv.STATE_DIR;
|
|
9
|
+
/**
|
|
10
|
+
* Where the daemon keeps pidfiles, the discovery registry and its logs.
|
|
11
|
+
*
|
|
12
|
+
* `~/.reticle` unless overridden. The override exists because a read-only $HOME is not hypothetical
|
|
13
|
+
* — a sandboxed agent or a locked-down Windows profile cannot write there, and until now that meant
|
|
14
|
+
* the daemon could not start at all. Read per call rather than captured at import, so a test (and a
|
|
15
|
+
* wrapper that sets it late) sees the current value.
|
|
16
|
+
*/
|
|
17
|
+
export function reticleStateHome() {
|
|
18
|
+
const override = process.env[STATE_DIR_ENV];
|
|
19
|
+
if (override !== undefined && override.length > 0)
|
|
20
|
+
return override;
|
|
21
|
+
return join(homedir(), '.reticle');
|
|
22
|
+
}
|
|
23
|
+
function pidPath(port, home = reticleStateHome()) {
|
|
8
24
|
return join(home, `daemon-${port}.pid`);
|
|
9
25
|
}
|
|
10
26
|
function registryPath(port) {
|
|
11
|
-
return join(
|
|
27
|
+
return join(reticleStateHome(), daemonRegistryFileName(port));
|
|
12
28
|
}
|
|
13
29
|
export function logPath(port) {
|
|
14
|
-
return join(
|
|
30
|
+
return join(reticleStateHome(), `daemon-${port}.log`);
|
|
15
31
|
}
|
|
16
|
-
|
|
32
|
+
/**
|
|
33
|
+
* How large a daemon log may get before it is rolled over to `<name>.1`.
|
|
34
|
+
*
|
|
35
|
+
* It was unbounded. A real dev machine reached **24MB** on one port, which is not merely untidy: it
|
|
36
|
+
* is the difference between a log somebody opens and a log somebody gives up on, and it is disk that
|
|
37
|
+
* nothing ever reclaims. One previous generation is kept, because the question people bring to this
|
|
38
|
+
* file ("what happened just now?") is answered by the current one and the question they bring next
|
|
39
|
+
* ("and just before that?") is answered by the other.
|
|
40
|
+
*/
|
|
41
|
+
export const MAX_DAEMON_LOG_BYTES = 8 * 1024 * 1024;
|
|
42
|
+
/**
|
|
43
|
+
* Roll the log over if it has grown past the cap. Best-effort: a daemon must still start when the
|
|
44
|
+
* log cannot be rotated, because failing to launch over housekeeping is strictly worse than a large
|
|
45
|
+
* file.
|
|
46
|
+
*/
|
|
47
|
+
export function rotateDaemonLog(path, deps, max = MAX_DAEMON_LOG_BYTES) {
|
|
48
|
+
try {
|
|
49
|
+
if (max >= deps.fileSize(path))
|
|
50
|
+
return false;
|
|
51
|
+
deps.renameFile(path, `${path}.1`);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function readPid(port, home = reticleStateHome()) {
|
|
17
59
|
const path = pidPath(port, home);
|
|
18
60
|
if (!existsSync(path))
|
|
19
61
|
return null;
|
|
@@ -30,7 +72,7 @@ export function isAlive(pid) {
|
|
|
30
72
|
}
|
|
31
73
|
}
|
|
32
74
|
export function writePid(port) {
|
|
33
|
-
mkdirSync(
|
|
75
|
+
mkdirSync(reticleStateHome(), { recursive: true });
|
|
34
76
|
writeFileSync(pidPath(port), String(process.pid), 'utf8');
|
|
35
77
|
}
|
|
36
78
|
/**
|
|
@@ -39,7 +81,7 @@ export function writePid(port) {
|
|
|
39
81
|
* the orphan-race guard: a losing childB (EADDRINUSE) must not delete the winning childA's live pidfile.
|
|
40
82
|
*/
|
|
41
83
|
export function shouldRemovePid(owner, expectedPid, alive) {
|
|
42
|
-
return
|
|
84
|
+
return null === owner || owner === expectedPid || !alive;
|
|
43
85
|
}
|
|
44
86
|
export function removePid(port, expectedPid = process.pid) {
|
|
45
87
|
const path = pidPath(port);
|
|
@@ -66,14 +108,14 @@ export function writeDaemonRegistry(port, meta) {
|
|
|
66
108
|
...(meta.projectId !== undefined ? { projectId: meta.projectId } : {}),
|
|
67
109
|
};
|
|
68
110
|
try {
|
|
69
|
-
mkdirSync(
|
|
111
|
+
mkdirSync(reticleStateHome(), { recursive: true });
|
|
70
112
|
writeFileSync(registryPath(port), JSON.stringify(entry), 'utf8');
|
|
71
113
|
}
|
|
72
114
|
catch {
|
|
73
115
|
// discovery is a convenience — never block startup on it
|
|
74
116
|
}
|
|
75
117
|
}
|
|
76
|
-
|
|
118
|
+
function removeDaemonRegistry(port) {
|
|
77
119
|
const path = registryPath(port);
|
|
78
120
|
try {
|
|
79
121
|
if (existsSync(path))
|
|
@@ -83,34 +125,6 @@ export function removeDaemonRegistry(port) {
|
|
|
83
125
|
// racing another cleaner — already gone
|
|
84
126
|
}
|
|
85
127
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Discover the port of a live daemon serving `projectId` by reading the registry entries in ~/.reticle.
|
|
88
|
-
* Returns null when none matches (caller falls back to the default port — never guesses a mismatched
|
|
89
|
-
* daemon). Stale entries (crashed daemons) are ignored via the pid liveness probe.
|
|
90
|
-
*/
|
|
91
|
-
export function discoverDaemonPortForProject(projectId) {
|
|
92
|
-
const entries = [];
|
|
93
|
-
let files;
|
|
94
|
-
try {
|
|
95
|
-
files = readdirSync(RETICLE_HOME);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return null; // no ~/.reticle yet
|
|
99
|
-
}
|
|
100
|
-
for (const file of files) {
|
|
101
|
-
if (daemonRegistryPort(file) === null)
|
|
102
|
-
continue;
|
|
103
|
-
try {
|
|
104
|
-
const parsed = DaemonRegistryEntrySchema.safeParse(JSON.parse(readFileSync(join(RETICLE_HOME, file), 'utf8')));
|
|
105
|
-
if (parsed.success)
|
|
106
|
-
entries.push(parsed.data);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// unreadable/corrupt entry — skip it
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return pickDaemonPort(entries, projectId, isAlive);
|
|
113
|
-
}
|
|
114
128
|
export function isRunning(port) {
|
|
115
129
|
const pid = readPid(port);
|
|
116
130
|
return pid !== null && isAlive(pid);
|
|
@@ -124,12 +138,12 @@ export function discoverDaemonPort() {
|
|
|
124
138
|
reclaimStaleDaemons(); // sweep crashed daemons' stale pidfiles before scanning for live ones
|
|
125
139
|
let found = null;
|
|
126
140
|
try {
|
|
127
|
-
for (const file of readdirSync(
|
|
141
|
+
for (const file of readdirSync(reticleStateHome())) {
|
|
128
142
|
const m = /^daemon-(\d+)\.pid$/.exec(file);
|
|
129
|
-
if (
|
|
143
|
+
if (null === m)
|
|
130
144
|
continue;
|
|
131
145
|
const port = Number(m[1]);
|
|
132
|
-
if (isRunning(port) && (
|
|
146
|
+
if (isRunning(port) && (null === found || port < found))
|
|
133
147
|
found = port;
|
|
134
148
|
}
|
|
135
149
|
}
|
|
@@ -144,7 +158,7 @@ export function discoverDaemonPort() {
|
|
|
144
158
|
* Returns the ports reclaimed. `home` and `pidAlive` are injectable for testing (default to the real
|
|
145
159
|
* ~/.reticle and the process.kill(pid,0) liveness probe).
|
|
146
160
|
*/
|
|
147
|
-
export function reclaimStaleDaemons(home =
|
|
161
|
+
export function reclaimStaleDaemons(home = reticleStateHome(), pidAlive = isAlive) {
|
|
148
162
|
const reclaimed = [];
|
|
149
163
|
let files;
|
|
150
164
|
try {
|
|
@@ -155,7 +169,7 @@ export function reclaimStaleDaemons(home = RETICLE_HOME, pidAlive = isAlive) {
|
|
|
155
169
|
}
|
|
156
170
|
for (const file of files) {
|
|
157
171
|
const match = /^daemon-(\d+)\.pid$/.exec(file);
|
|
158
|
-
if (
|
|
172
|
+
if (null === match)
|
|
159
173
|
continue;
|
|
160
174
|
const path = join(home, file);
|
|
161
175
|
let pid = null;
|
|
@@ -167,7 +181,7 @@ export function reclaimStaleDaemons(home = RETICLE_HOME, pidAlive = isAlive) {
|
|
|
167
181
|
catch {
|
|
168
182
|
pid = null; // unreadable pidfile counts as stale
|
|
169
183
|
}
|
|
170
|
-
if (
|
|
184
|
+
if (null === pid || !pidAlive(pid)) {
|
|
171
185
|
try {
|
|
172
186
|
unlinkSync(path);
|
|
173
187
|
removeDaemonRegistry(Number(match[1])); // drop the sidecar discovery entry too
|
|
@@ -180,11 +194,13 @@ export function reclaimStaleDaemons(home = RETICLE_HOME, pidAlive = isAlive) {
|
|
|
180
194
|
}
|
|
181
195
|
return reclaimed;
|
|
182
196
|
}
|
|
183
|
-
|
|
197
|
+
function defaultSpawnDaemonDeps() {
|
|
184
198
|
return {
|
|
185
|
-
home:
|
|
199
|
+
home: reticleStateHome(),
|
|
186
200
|
openFile: openSync,
|
|
187
201
|
closeFile: closeSync,
|
|
202
|
+
fileSize: (path) => (existsSync(path) ? statSync(path).size : 0),
|
|
203
|
+
renameFile: renameSync,
|
|
188
204
|
spawnChild: (command, args, options) => spawn(command, [...args], { detached: options.detached, stdio: [...options.stdio] }),
|
|
189
205
|
pidAlive: isAlive,
|
|
190
206
|
};
|
|
@@ -198,7 +214,20 @@ export function defaultSpawnDaemonDeps() {
|
|
|
198
214
|
* pattern as reclaimStaleDaemons(home, pidAlive).
|
|
199
215
|
*/
|
|
200
216
|
export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpawnDaemonDeps()) {
|
|
201
|
-
|
|
217
|
+
try {
|
|
218
|
+
mkdirSync(deps.home, { recursive: true });
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
// The log-file open below has always been guarded; this was not, so the ONE failure a sandboxed
|
|
222
|
+
// agent actually hits — a read-only $HOME — escaped as a raw EACCES naming nothing. Refuse
|
|
223
|
+
// loudly, and name the way out.
|
|
224
|
+
log('reticle_daemon_state_unwritable', {
|
|
225
|
+
home: deps.home,
|
|
226
|
+
error: err instanceof Error ? err.message : String(err),
|
|
227
|
+
fix: `Reticle could not create its state directory. Set ${STATE_DIR_ENV} to a writable path and retry.`,
|
|
228
|
+
});
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
202
231
|
const pidFilePath = join(deps.home, `daemon-${port}.pid`);
|
|
203
232
|
const logFilePath = join(deps.home, `daemon-${port}.log`);
|
|
204
233
|
// O_EXCL spawn-lock: only the FIRST racer to create the pidfile spawns. A concurrent second gets
|
|
@@ -220,6 +249,9 @@ export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpaw
|
|
|
220
249
|
return false; // lost a concurrent reclaim race
|
|
221
250
|
}
|
|
222
251
|
}
|
|
252
|
+
// Roll the log BEFORE opening the append handle, or the daemon writes into the renamed file
|
|
253
|
+
// through an fd that no longer matches any name anyone can find.
|
|
254
|
+
rotateDaemonLog(logFilePath, deps);
|
|
223
255
|
let logFd;
|
|
224
256
|
try {
|
|
225
257
|
logFd = deps.openFile(logFilePath, 'a');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long an idle daemon is given before it shuts itself down.
|
|
3
|
+
*
|
|
4
|
+
* It used to be one number for every case, and at 5 minutes that number killed live runs: 187
|
|
5
|
+
* `reticle_daemon_idle_shutdown` lines on one user's machine, with their editor's Reticle
|
|
6
|
+
* disconnecting each time; and in the fixtures gate it fired during long dependency installs, so apps
|
|
7
|
+
* that booted afterwards hit ERR_CONNECTION_REFUSED and were scored as install failures when nothing
|
|
8
|
+
* was wrong with their install.
|
|
9
|
+
*
|
|
10
|
+
* Reverting is not an option. Daemons used to sit idle a median of 28 minutes at a 0.04% duty cycle
|
|
11
|
+
* because "an agent is attached" alone kept one alive for a whole editor session, and the widened
|
|
12
|
+
* rule is what fixed that.
|
|
13
|
+
*
|
|
14
|
+
* The distinction is TIME, not state. Mid-install an attached daemon is state-identical to one in an
|
|
15
|
+
* empty directory — nothing has been asked of either. What differs is how long it is reasonable to
|
|
16
|
+
* wait: quiet with a client attached means the human is thinking or the install is slow; the same
|
|
17
|
+
* quiet with nobody attached means the daemon is unwanted. Both still exit.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* An attached daemon waits this many times the base grace. Six is chosen against the two measurements
|
|
21
|
+
* that matter: the default base is 5 minutes, so an attached daemon survives 30 — comfortably past
|
|
22
|
+
* the slow installs that were killing runs, and comfortably short of the 28-minute median idle the
|
|
23
|
+
* shorter rule was introduced to end.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ATTACHED_GRACE_MULTIPLIER = 6;
|
|
26
|
+
/**
|
|
27
|
+
* @param overrideMs an explicit attached grace (RETICLE_IDLE_ATTACHED_MS). Derived from the base when
|
|
28
|
+
* absent — but the derived value is arithmetic on a number the caller also controls, and a test that
|
|
29
|
+
* has to reason about both is a test that breaks for the wrong reason. Configuring it directly is
|
|
30
|
+
* what lets the e2e spec assert the BEHAVIOUR (an attached daemon exits) without racing a product.
|
|
31
|
+
*/
|
|
32
|
+
export declare function idleGraceMs(baseMs: number, agentAttached: boolean, overrideMs?: number): number;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long an idle daemon is given before it shuts itself down.
|
|
3
|
+
*
|
|
4
|
+
* It used to be one number for every case, and at 5 minutes that number killed live runs: 187
|
|
5
|
+
* `reticle_daemon_idle_shutdown` lines on one user's machine, with their editor's Reticle
|
|
6
|
+
* disconnecting each time; and in the fixtures gate it fired during long dependency installs, so apps
|
|
7
|
+
* that booted afterwards hit ERR_CONNECTION_REFUSED and were scored as install failures when nothing
|
|
8
|
+
* was wrong with their install.
|
|
9
|
+
*
|
|
10
|
+
* Reverting is not an option. Daemons used to sit idle a median of 28 minutes at a 0.04% duty cycle
|
|
11
|
+
* because "an agent is attached" alone kept one alive for a whole editor session, and the widened
|
|
12
|
+
* rule is what fixed that.
|
|
13
|
+
*
|
|
14
|
+
* The distinction is TIME, not state. Mid-install an attached daemon is state-identical to one in an
|
|
15
|
+
* empty directory — nothing has been asked of either. What differs is how long it is reasonable to
|
|
16
|
+
* wait: quiet with a client attached means the human is thinking or the install is slow; the same
|
|
17
|
+
* quiet with nobody attached means the daemon is unwanted. Both still exit.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* An attached daemon waits this many times the base grace. Six is chosen against the two measurements
|
|
21
|
+
* that matter: the default base is 5 minutes, so an attached daemon survives 30 — comfortably past
|
|
22
|
+
* the slow installs that were killing runs, and comfortably short of the 28-minute median idle the
|
|
23
|
+
* shorter rule was introduced to end.
|
|
24
|
+
*/
|
|
25
|
+
export const ATTACHED_GRACE_MULTIPLIER = 6;
|
|
26
|
+
/**
|
|
27
|
+
* @param overrideMs an explicit attached grace (RETICLE_IDLE_ATTACHED_MS). Derived from the base when
|
|
28
|
+
* absent — but the derived value is arithmetic on a number the caller also controls, and a test that
|
|
29
|
+
* has to reason about both is a test that breaks for the wrong reason. Configuring it directly is
|
|
30
|
+
* what lets the e2e spec assert the BEHAVIOUR (an attached daemon exits) without racing a product.
|
|
31
|
+
*/
|
|
32
|
+
export function idleGraceMs(baseMs, agentAttached, overrideMs) {
|
|
33
|
+
// A non-positive base means the watcher is disabled; multiplying must not resurrect it.
|
|
34
|
+
if (baseMs <= 0)
|
|
35
|
+
return baseMs;
|
|
36
|
+
if (!agentAttached)
|
|
37
|
+
return baseMs;
|
|
38
|
+
return overrideMs !== undefined && overrideMs > 0
|
|
39
|
+
? overrideMs
|
|
40
|
+
: baseMs * ATTACHED_GRACE_MULTIPLIER;
|
|
41
|
+
}
|
|
@@ -5,6 +5,14 @@ export interface IdleShutdownOptions {
|
|
|
5
5
|
onShutdown: () => void;
|
|
6
6
|
/** Continuous-idle window before shutdown. `0` (or negative) disables the watcher entirely. */
|
|
7
7
|
graceMs: number;
|
|
8
|
+
/**
|
|
9
|
+
* Is an agent's MCP client attached right now? An attached daemon gets a much longer grace — quiet
|
|
10
|
+
* with a client present means a slow install or a thinking human, not an unwanted daemon. It still
|
|
11
|
+
* exits. See idle-grace.
|
|
12
|
+
*/
|
|
13
|
+
agentAttached?: () => boolean;
|
|
14
|
+
/** Explicit attached grace (RETICLE_IDLE_ATTACHED_MS); derived from graceMs when absent. */
|
|
15
|
+
attachedGraceMs?: number;
|
|
8
16
|
/** How often to re-check idleness. */
|
|
9
17
|
checkIntervalMs?: number;
|
|
10
18
|
/** Injected clock — never read wall-clock time in logic (repo rule). */
|
|
@@ -30,4 +38,9 @@ export declare class IdleShutdown {
|
|
|
30
38
|
* Resolve the idle-shutdown grace from `RETICLE_IDLE_SHUTDOWN_MS`: a non-negative integer of milliseconds,
|
|
31
39
|
* `0` to disable. Anything missing/invalid falls back to the default. Pure.
|
|
32
40
|
*/
|
|
41
|
+
/**
|
|
42
|
+
* The idle re-check cadence, from the environment. Same shape as the grace resolver: a bad value
|
|
43
|
+
* falls back rather than throwing, and 0/negative is not a legal cadence (it would spin).
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveIdleCheckMs(raw: string | undefined): number;
|
|
33
46
|
export declare function resolveIdleShutdownMs(raw: string | undefined): number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SESSION_LIFECYCLE } from '@reticlehq/core';
|
|
2
|
-
import {
|
|
2
|
+
import { idleGraceMs } from './idle-grace.js';
|
|
3
|
+
import { log } from '../log.js';
|
|
3
4
|
/**
|
|
4
5
|
* Self-terminates an idle daemon so Reticle never lingers eating a user's resources (the daemon process,
|
|
5
6
|
* any headless Chromium the pool launched, and the bound port) after the editor closes. The `reticle mcp`
|
|
@@ -15,12 +16,16 @@ export class IdleShutdown {
|
|
|
15
16
|
#isIdle;
|
|
16
17
|
#onShutdown;
|
|
17
18
|
#graceMs;
|
|
19
|
+
#agentAttached;
|
|
20
|
+
#attachedGraceMs;
|
|
18
21
|
#checkMs;
|
|
19
22
|
#clock;
|
|
20
23
|
constructor(opts) {
|
|
21
24
|
this.#isIdle = opts.isIdle;
|
|
22
25
|
this.#onShutdown = opts.onShutdown;
|
|
23
26
|
this.#graceMs = opts.graceMs;
|
|
27
|
+
this.#agentAttached = opts.agentAttached;
|
|
28
|
+
this.#attachedGraceMs = opts.attachedGraceMs;
|
|
24
29
|
this.#checkMs = opts.checkIntervalMs ?? SESSION_LIFECYCLE.DAEMON_IDLE_CHECK_MS;
|
|
25
30
|
this.#clock = opts.clock ?? (() => Date.now());
|
|
26
31
|
}
|
|
@@ -34,9 +39,15 @@ export class IdleShutdown {
|
|
|
34
39
|
}
|
|
35
40
|
const now = this.#clock();
|
|
36
41
|
this.#idleSince ??= now;
|
|
37
|
-
|
|
42
|
+
// Read per CHECK, not at construction: a client attaches and detaches during a daemon's life, and
|
|
43
|
+
// the question "how long should this quiet be tolerated" is only answerable now. See idle-grace.
|
|
44
|
+
const grace = idleGraceMs(this.#graceMs, this.#agentAttached?.() ?? false, this.#attachedGraceMs);
|
|
45
|
+
if (now - this.#idleSince >= grace) {
|
|
38
46
|
this.#fired = true;
|
|
39
|
-
log('reticle_daemon_idle_shutdown', {
|
|
47
|
+
log('reticle_daemon_idle_shutdown', {
|
|
48
|
+
idleMs: now - this.#idleSince,
|
|
49
|
+
attached: this.#agentAttached?.() ?? false,
|
|
50
|
+
});
|
|
40
51
|
this.#onShutdown();
|
|
41
52
|
}
|
|
42
53
|
}
|
|
@@ -57,8 +68,20 @@ export class IdleShutdown {
|
|
|
57
68
|
* Resolve the idle-shutdown grace from `RETICLE_IDLE_SHUTDOWN_MS`: a non-negative integer of milliseconds,
|
|
58
69
|
* `0` to disable. Anything missing/invalid falls back to the default. Pure.
|
|
59
70
|
*/
|
|
71
|
+
/**
|
|
72
|
+
* The idle re-check cadence, from the environment. Same shape as the grace resolver: a bad value
|
|
73
|
+
* falls back rather than throwing, and 0/negative is not a legal cadence (it would spin).
|
|
74
|
+
*/
|
|
75
|
+
export function resolveIdleCheckMs(raw) {
|
|
76
|
+
if (raw === undefined || '' === raw.trim())
|
|
77
|
+
return SESSION_LIFECYCLE.DAEMON_IDLE_CHECK_MS;
|
|
78
|
+
const n = Number(raw);
|
|
79
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
80
|
+
return SESSION_LIFECYCLE.DAEMON_IDLE_CHECK_MS;
|
|
81
|
+
return Math.floor(n);
|
|
82
|
+
}
|
|
60
83
|
export function resolveIdleShutdownMs(raw) {
|
|
61
|
-
if (raw === undefined || raw.trim()
|
|
84
|
+
if (raw === undefined || '' === raw.trim())
|
|
62
85
|
return SESSION_LIFECYCLE.DAEMON_IDLE_SHUTDOWN_MS;
|
|
63
86
|
const n = Number(raw);
|
|
64
87
|
if (!Number.isFinite(n) || n < 0)
|
|
@@ -107,11 +107,11 @@ export function buildDomainModel(flows, contract, runs = []) {
|
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
109
|
function buildSummary(flowCount, coverage, gaps, topRisk) {
|
|
110
|
-
if (
|
|
110
|
+
if (0 === flowCount) {
|
|
111
111
|
return 'No saved flows yet — record the critical journeys (reticle_record{action:"start"}) so the agent learns the app.';
|
|
112
112
|
}
|
|
113
113
|
const parts = [
|
|
114
|
-
`${String(flowCount)} flow${
|
|
114
|
+
`${String(flowCount)} flow${1 === flowCount ? '' : 's'}: ${String(coverage.asserted)} asserted, ${String(coverage.presenceOnly)} presence-only, ${String(coverage.assertionFree)} assertion-free`,
|
|
115
115
|
];
|
|
116
116
|
if (topRisk !== undefined) {
|
|
117
117
|
parts.push(`test first: ${topRisk.name} (${topRisk.reason})`);
|
|
@@ -25,19 +25,19 @@ const ERROR_KEYS = ['error', 'errors', 'errorMessage', 'failureReason'];
|
|
|
25
25
|
/** Arrays worth walking for per-item outcomes. Bulk APIs converge on a handful of names. */
|
|
26
26
|
const ITEM_KEYS = ['results', 'items', 'data', 'records', 'responses'];
|
|
27
27
|
function isRecord(value) {
|
|
28
|
-
return typeof value
|
|
28
|
+
return 'object' === typeof value && value !== null && !Array.isArray(value);
|
|
29
29
|
}
|
|
30
30
|
/** Whether one object reports a failure of its own. */
|
|
31
31
|
function reportsFailure(item) {
|
|
32
32
|
if (!isRecord(item))
|
|
33
33
|
return false;
|
|
34
34
|
for (const flag of FAILURE_FLAGS) {
|
|
35
|
-
if (item[flag]
|
|
35
|
+
if (false === item[flag])
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
38
|
for (const key of ERROR_KEYS) {
|
|
39
39
|
const value = item[key];
|
|
40
|
-
if (value === undefined ||
|
|
40
|
+
if (value === undefined || null === value)
|
|
41
41
|
continue;
|
|
42
42
|
if (Array.isArray(value)) {
|
|
43
43
|
if (value.length > 0)
|
|
@@ -45,7 +45,7 @@ function reportsFailure(item) {
|
|
|
45
45
|
continue;
|
|
46
46
|
}
|
|
47
47
|
// An empty string or an explicit null is an API saying "no error", not an error.
|
|
48
|
-
if (typeof value
|
|
48
|
+
if ('string' === typeof value && 0 === value.length)
|
|
49
49
|
continue;
|
|
50
50
|
return true;
|
|
51
51
|
}
|
|
@@ -55,7 +55,7 @@ function reportsFailure(item) {
|
|
|
55
55
|
function itemOutcomes(body) {
|
|
56
56
|
for (const key of ITEM_KEYS) {
|
|
57
57
|
const list = body[key];
|
|
58
|
-
if (!Array.isArray(list) || list.length
|
|
58
|
+
if (!Array.isArray(list) || 0 === list.length)
|
|
59
59
|
continue;
|
|
60
60
|
const failed = list.filter(reportsFailure).length;
|
|
61
61
|
return { failed, total: list.length };
|
|
@@ -63,7 +63,7 @@ function itemOutcomes(body) {
|
|
|
63
63
|
return { failed: 0, total: 0 };
|
|
64
64
|
}
|
|
65
65
|
function parse(raw) {
|
|
66
|
-
if (typeof raw !== 'string' || raw.length
|
|
66
|
+
if (typeof raw !== 'string' || 0 === raw.length)
|
|
67
67
|
return undefined;
|
|
68
68
|
try {
|
|
69
69
|
const parsed = JSON.parse(raw);
|
|
@@ -87,8 +87,8 @@ export function findBodyFailures(events) {
|
|
|
87
87
|
const body = parse(event.data['responseBody']);
|
|
88
88
|
if (body === undefined)
|
|
89
89
|
continue;
|
|
90
|
-
const url = typeof event.data['url']
|
|
91
|
-
const method = typeof event.data['method']
|
|
90
|
+
const url = 'string' === typeof event.data['url'] ? event.data['url'] : '';
|
|
91
|
+
const method = 'string' === typeof event.data['method'] ? event.data['method'] : '';
|
|
92
92
|
const where = `${method} ${url} → ${String(status)}`;
|
|
93
93
|
const { failed, total } = itemOutcomes(body);
|
|
94
94
|
if (failed > 0) {
|
|
@@ -50,4 +50,4 @@ export interface ContradictionOptions {
|
|
|
50
50
|
*/
|
|
51
51
|
mutatedWithin?: number | undefined;
|
|
52
52
|
}
|
|
53
|
-
export declare function findContradictions(
|
|
53
|
+
export declare function findContradictions(allEvents: readonly ReticleEvent[], options?: ContradictionOptions): Contradiction[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContradictionKind, EventType, MUTATING_METHODS } from '@reticlehq/core';
|
|
1
|
+
import { ContradictionKind, EventType, MUTATING_METHODS, isDevToolingUrl, } from '@reticlehq/core';
|
|
2
2
|
import { findStaleResponses } from './stale-response.js';
|
|
3
3
|
import { findBodyFailures } from './body-failures.js';
|
|
4
4
|
import { findEchoMismatches } from './echo-mismatch.js';
|
|
@@ -14,7 +14,7 @@ function netCall(e) {
|
|
|
14
14
|
// Neither present = no verdict was ever reported, which stays undefined all the way through.
|
|
15
15
|
ok: e.data['ok'] === undefined && status === undefined
|
|
16
16
|
? undefined
|
|
17
|
-
: e.data['ok']
|
|
17
|
+
: true === e.data['ok'] || (e.data['ok'] === undefined && (status ?? 0) < 400),
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
@@ -95,7 +95,7 @@ function failureAcknowledged(events) {
|
|
|
95
95
|
return false;
|
|
96
96
|
const path = asString(e.data['path']) ?? '';
|
|
97
97
|
const value = e.data['value'];
|
|
98
|
-
return ACKNOWLEDGED.test(path) || (typeof value
|
|
98
|
+
return ACKNOWLEDGED.test(path) || ('string' === typeof value && ACKNOWLEDGED.test(value));
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
function describe(call) {
|
|
@@ -125,13 +125,44 @@ const MUST_DO_SOMETHING = new Set(['click', 'dblclick', 'submit']);
|
|
|
125
125
|
*/
|
|
126
126
|
function didNothing(events, requests, mutatedWithin) {
|
|
127
127
|
if (mutatedWithin === undefined)
|
|
128
|
-
return events.length
|
|
128
|
+
return 0 === events.length;
|
|
129
129
|
if (mutatedWithin > 0 || requests.length > 0)
|
|
130
130
|
return false;
|
|
131
131
|
return !events.some((e) => e.type === EventType.ROUTE_CHANGE || e.type === EventType.VISIBLE_SHOWN);
|
|
132
132
|
}
|
|
133
|
-
|
|
133
|
+
/** Net-shaped events — the only ones that carry a URL a dev-tooling channel could occupy. */
|
|
134
|
+
const NET_TYPES = new Set([
|
|
135
|
+
EventType.NET_PENDING,
|
|
136
|
+
EventType.NET_REQUEST,
|
|
137
|
+
EventType.NET_STREAM,
|
|
138
|
+
]);
|
|
139
|
+
/**
|
|
140
|
+
* Split the window into the app's traffic and the dev toolchain's own (see `DevToolingChannel`).
|
|
141
|
+
*
|
|
142
|
+
* NOTHING below may judge the toolchain. Reported from a real drive: a correct Next.js navigation
|
|
143
|
+
* graded `verified: "no"` because the dev overlay was resolving a source map for an unrelated React
|
|
144
|
+
* key warning, and that in-flight `POST /__nextjs_original-stack-frames` read as "the UI advanced
|
|
145
|
+
* over a request that never settled". Every app that logs one dev warning got a false negative on
|
|
146
|
+
* every action. The overlay's own 404s and duplicate fetches are the same story on other checks,
|
|
147
|
+
* which is why the split happens ONCE here rather than in the one check that reported it.
|
|
148
|
+
*/
|
|
149
|
+
function splitDevTooling(events) {
|
|
150
|
+
const ignored = [];
|
|
151
|
+
const app = events.filter((e) => {
|
|
152
|
+
if (!NET_TYPES.has(e.type))
|
|
153
|
+
return true;
|
|
154
|
+
const url = asString(e.data['url']);
|
|
155
|
+
if (!isDevToolingUrl(url))
|
|
156
|
+
return true;
|
|
157
|
+
if (url !== undefined && !ignored.includes(url))
|
|
158
|
+
ignored.push(url);
|
|
159
|
+
return false;
|
|
160
|
+
});
|
|
161
|
+
return { app, ignored };
|
|
162
|
+
}
|
|
163
|
+
export function findContradictions(allEvents, options = {}) {
|
|
134
164
|
const found = [];
|
|
165
|
+
const { app: events, ignored: ignoredDevTooling } = splitDevTooling(allEvents);
|
|
135
166
|
const settled = events.filter((e) => e.type === EventType.NET_REQUEST).map(netCall);
|
|
136
167
|
// ── Overlapping reads that settled out of order ─────────────────────────────────────────────
|
|
137
168
|
// Independent of any action, so it runs on every window: the race is a property of the timeline.
|
|
@@ -160,7 +191,7 @@ export function findContradictions(events, options = {}) {
|
|
|
160
191
|
},
|
|
161
192
|
];
|
|
162
193
|
}
|
|
163
|
-
const failed = settled.filter((c) => c.ok
|
|
194
|
+
const failed = settled.filter((c) => false === c.ok);
|
|
164
195
|
const advanced = uiAdvanced(events);
|
|
165
196
|
const signals = events
|
|
166
197
|
.filter((e) => e.type === EventType.SIGNAL)
|
|
@@ -170,7 +201,19 @@ export function findContradictions(events, options = {}) {
|
|
|
170
201
|
// the control worked, the DESTINATION is empty — which is why every "did the click do something"
|
|
171
202
|
// heuristic passes it, a route change being unambiguously something.
|
|
172
203
|
const routed = events.some((e) => e.type === EventType.ROUTE_CHANGE);
|
|
173
|
-
|
|
204
|
+
// `dom.text` counts as rendered, and it has to: React reconciles a destination IN PLACE far more
|
|
205
|
+
// often than it adds nodes. Measured on three ordinary sidebar navigations of the bench app — every
|
|
206
|
+
// one emitted { dom.attr:2, dom.text:2, render.commit, state.change } and ZERO dom.added/removed,
|
|
207
|
+
// so all three were flagged as blank destinations, `verified` came back "no" on a correct green,
|
|
208
|
+
// and a bug_found was emitted for a navigation that worked.
|
|
209
|
+
//
|
|
210
|
+
// Deliberately NOT `dom.attr`: the nav link marks itself active whether or not the destination
|
|
211
|
+
// rendered, which is the true positive this rule exists for. Deliberately NOT `render.commit`
|
|
212
|
+
// either: React commits a render for a component that returns null, which is one of the very bugs
|
|
213
|
+
// named in `detail` below.
|
|
214
|
+
const rendered = events.some((e) => e.type === EventType.DOM_ADDED ||
|
|
215
|
+
e.type === EventType.DOM_REMOVED ||
|
|
216
|
+
e.type === EventType.DOM_TEXT);
|
|
174
217
|
const fetched = events.some((e) => e.type === EventType.NET_REQUEST || e.type === EventType.NET_PENDING);
|
|
175
218
|
if (routed && !rendered && !fetched) {
|
|
176
219
|
found.push({
|
|
@@ -187,7 +230,7 @@ export function findContradictions(events, options = {}) {
|
|
|
187
230
|
// Checked BEFORE the success-claim rules, because it is the sharper reading of the same events:
|
|
188
231
|
// the app did not claim success, it claimed the wrong failure. Telling someone their password is
|
|
189
232
|
// wrong while the backend is down sends them to fix something they cannot fix.
|
|
190
|
-
const serverFaults = settled.filter((c) => c.ok
|
|
233
|
+
const serverFaults = settled.filter((c) => false === c.ok && c.status !== undefined && c.status >= SERVER_FAULT_MIN);
|
|
191
234
|
const userBlame = [
|
|
192
235
|
...signals.filter((name) => BLAMES_USER.test(name)),
|
|
193
236
|
...events
|
|
@@ -228,7 +271,7 @@ export function findContradictions(events, options = {}) {
|
|
|
228
271
|
// Writes only: a GET that changes nothing is a prefetch; a POST that changes nothing is a lost
|
|
229
272
|
// write, a response parsed into the void, or a render that never happened.
|
|
230
273
|
if (!advanced) {
|
|
231
|
-
const ignoredWrites = settled.filter((c) => c.ok
|
|
274
|
+
const ignoredWrites = settled.filter((c) => true === c.ok && isMutating(c));
|
|
232
275
|
if (ignoredWrites.length > 0) {
|
|
233
276
|
found.push({
|
|
234
277
|
kind: ContradictionKind.RESPONSE_IGNORED,
|
|
@@ -274,7 +317,14 @@ export function findContradictions(events, options = {}) {
|
|
|
274
317
|
kind: ContradictionKind.REQUEST_NEVER_SETTLED,
|
|
275
318
|
claim: 'the UI moved forward and the action reported done',
|
|
276
319
|
counter: `${String(inFlight.length)} request(s) were still in flight`,
|
|
277
|
-
|
|
320
|
+
// The exclusion is never silent: if the toolchain's own traffic was dropped from this count,
|
|
321
|
+
// the finding says which URLs, so an agent reading it can see what Reticle chose to ignore.
|
|
322
|
+
detail: [
|
|
323
|
+
inFlight.map((p) => describe(p.call)).join('; '),
|
|
324
|
+
...(0 === ignoredDevTooling.length
|
|
325
|
+
? []
|
|
326
|
+
: [`ignored as dev tooling: ${ignoredDevTooling.join(', ')}`]),
|
|
327
|
+
].join(' — '),
|
|
278
328
|
});
|
|
279
329
|
}
|
|
280
330
|
}
|