@reticlehq/server 2.3.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} +105 -15
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +6 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +21 -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/cli-parse.d.ts +143 -0
- package/dist/{cli-parse.js → cli/cli-parse.js} +146 -65
- package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +15 -0
- package/dist/{cli-port.js → cli/cli-port.js} +29 -8
- 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 +182 -93
- 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.d.ts +0 -6
- package/dist/flows/decision.js +64 -8
- 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 +67 -13
- 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.d.ts +17 -0
- package/dist/init/capabilities.js +54 -0
- 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 +26 -0
- package/dist/init/detect.js +78 -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 +39 -0
- package/dist/init/next-patch.js +196 -0
- package/dist/init/node-io.js +48 -6
- package/dist/init/patch-kind.d.ts +20 -0
- package/dist/init/patch-kind.js +10 -0
- package/dist/init/plan-framework.d.ts +50 -0
- package/dist/init/plan-framework.js +341 -0
- package/dist/init/plan.d.ts +119 -6
- package/dist/init/plan.js +230 -131
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +22 -0
- package/dist/init/run.js +393 -28
- package/dist/init/slash-command.d.ts +27 -0
- package/dist/init/slash-command.js +61 -0
- package/dist/init/snippets.d.ts +41 -3
- package/dist/init/snippets.js +185 -10
- package/dist/init/vite-config.d.ts +4 -10
- package/dist/init/vite-config.js +15 -7
- package/dist/init/workspace-apps.d.ts +23 -0
- package/dist/init/workspace-apps.js +68 -0
- package/dist/input/driven-redaction.d.ts +15 -0
- package/dist/input/driven-redaction.js +76 -0
- package/dist/input/network-detail.d.ts +9 -3
- package/dist/input/network-detail.js +43 -23
- 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 +58 -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 +20 -2
- package/dist/session/session.js +76 -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 +85 -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/cli-parse.d.ts +0 -140
- package/dist/daemon-resilience.d.ts +0 -22
- package/dist/daemon-resilience.js +0 -85
- package/dist/mcp-proxy.d.ts +0 -35
- package/dist/mcp-proxy.js +0 -190
- 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,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
|
|
@@ -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[];
|