@reticlehq/server 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +30 -28
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +37 -5
- package/dist/command/cli/cli-flow-commands.d.ts +2 -1
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +38 -7
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +131 -19
- package/dist/command/cli/cloud-cli.js +7 -6
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +3 -7
- package/dist/command/cli/config/config-discovery.d.ts +12 -0
- package/dist/command/cli/config/config-discovery.js +19 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
- package/dist/command/cli/doctor/doctor-rows.js +12 -0
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +1 -1
- package/dist/command/cli/launch/cli-launch.d.ts +3 -23
- package/dist/command/cli/launch/cli-launch.js +29 -70
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
- package/dist/command/cli/mcp-command.js +5 -3
- package/dist/command/cli/ports/resolve/cli-port.d.ts +15 -1
- package/dist/command/cli/ports/resolve/cli-port.js +50 -2
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli/status/status-lines.d.ts +28 -0
- package/dist/command/cli/status/status-lines.js +117 -0
- package/dist/command/cli.d.ts +1 -1
- package/dist/command/cli.js +29 -177
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +32 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +93 -0
- package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
- package/dist/command/daemon/binding/loopback-reach.js +63 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/daemon-resilience.d.ts +13 -1
- package/dist/command/daemon/daemon-resilience.js +12 -1
- package/dist/command/daemon/daemon.d.ts +2 -11
- package/dist/command/daemon/daemon.js +8 -11
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
- package/dist/command/daemon/lifetime/heartbeat.js +10 -0
- package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
- package/dist/command/daemon/lifetime/log-cap.js +115 -0
- package/dist/command/setup/agent-notice-memory.d.ts +16 -0
- package/dist/command/setup/agent-notice-memory.js +65 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
- package/dist/command/setup/bringup/dev-server-wait.js +16 -0
- package/dist/command/setup/bringup/ensure-daemon.js +1 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/command/setup/init/init-runtime.js +13 -1
- package/dist/command/setup/node-effects.js +1 -1
- package/dist/command/setup/probe/page-probe.js +9 -2
- package/dist/command/setup/run-setup.d.ts +25 -7
- package/dist/command/setup/run-setup.js +51 -9
- package/dist/command/setup/setup-command.js +31 -3
- package/dist/command/setup/terminal/confirm.js +2 -1
- package/dist/command/update/updater.d.ts +27 -0
- package/dist/command/update/updater.js +35 -2
- package/dist/features/crawl/crawl-tools.js +3 -1
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +3 -1
- package/dist/features/harness/drive-report.d.ts +73 -0
- package/dist/features/harness/drive-report.js +196 -0
- package/dist/features/harness/driver.js +29 -8
- package/dist/features/harness/drivers.d.ts +19 -0
- package/dist/features/harness/drivers.js +19 -0
- package/dist/features/harness/fill-values.d.ts +67 -0
- package/dist/features/harness/fill-values.js +153 -0
- package/dist/features/harness/jev-driver.d.ts +188 -0
- package/dist/features/harness/jev-driver.js +661 -0
- package/dist/features/harness/openai-driver.d.ts +46 -0
- package/dist/features/harness/openai-driver.js +197 -0
- package/dist/features/harness/platform-config.d.ts +62 -0
- package/dist/features/harness/platform-config.js +97 -0
- package/dist/features/visual/visual-tools.js +26 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +80 -130
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/domain/domain-tools.js +4 -2
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/judgement/runs/verification-sync.d.ts +2 -2
- package/dist/judgement/runs/verification-sync.js +9 -2
- package/dist/language/flows/affected-tools.js +6 -3
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +55 -10
- package/dist/language/flows/flow-replay-run.js +217 -58
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +29 -11
- package/dist/language/flows/flow-replay.js +221 -203
- package/dist/language/flows/flow-result.d.ts +18 -1
- package/dist/language/flows/flow-result.js +18 -2
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-store-for-session.d.ts +2 -1
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flow-tools.js +8 -4
- package/dist/language/flows/flows.d.ts +15 -10
- package/dist/language/flows/flows.js +17 -4
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/narrow-write.d.ts +4 -4
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +13 -9
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/suite-selection.d.ts +15 -1
- package/dist/language/flows/suite-selection.js +1 -1
- package/dist/language/flows/verify-change-tools.js +21 -4
- package/dist/memory/cloud/account-state.d.ts +1 -1
- package/dist/memory/cloud/account-state.js +5 -4
- package/dist/memory/cloud/cloud-config.d.ts +9 -0
- package/dist/memory/cloud/cloud-config.js +9 -0
- package/dist/memory/cloud/cloud-sync.d.ts +7 -4
- package/dist/memory/cloud/cloud-sync.js +11 -6
- package/dist/memory/cloud/harness-config.d.ts +12 -0
- package/dist/memory/cloud/harness-config.js +58 -0
- package/dist/memory/cloud/harness-offer.d.ts +44 -0
- package/dist/memory/cloud/harness-offer.js +146 -0
- package/dist/memory/cloud/harness-switch.d.ts +11 -0
- package/dist/memory/cloud/harness-switch.js +53 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +35 -2
- package/dist/memory/cloud/sync-daemon.js +45 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +2 -1
- package/dist/memory/impact/impact-store.d.ts +5 -0
- package/dist/memory/impact/impact-store.js +25 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +253 -141
- package/dist/memory/intent/intent-shard.d.ts +22 -23
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/attach-journal.d.ts +11 -0
- package/dist/memory/journal/attach-journal.js +1 -1
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-query.d.ts +35 -1
- package/dist/memory/journal/journal-query.js +46 -0
- package/dist/memory/journal/journal-recorder.d.ts +71 -2
- package/dist/memory/journal/journal-recorder.js +45 -7
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +91 -2
- package/dist/memory/journal/on-disk/retention.js +181 -11
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -2
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -38
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/self-observation.d.ts +3 -0
- package/dist/memory/journal/self-observation.js +67 -0
- package/dist/memory/journal/session-end.d.ts +17 -4
- package/dist/memory/journal/session-end.js +85 -7
- package/dist/memory/journal/session-journal.d.ts +122 -5
- package/dist/memory/journal/session-journal.js +489 -32
- package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
- package/dist/memory/project/artifact-root-resolver.js +89 -0
- package/dist/memory/project/artifact-root.d.ts +39 -2
- package/dist/memory/project/artifact-root.js +58 -1
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
- package/dist/memory/project/dir/fill-value-store.js +88 -0
- package/dist/memory/project/dir/reticle-dir.d.ts +15 -5
- package/dist/memory/project/dir/reticle-dir.js +11 -0
- package/dist/memory/project/fs/fs-port.d.ts +22 -1
- package/dist/memory/project/fs/fs-port.js +72 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/project/learned-routes.d.ts +16 -2
- package/dist/memory/project/learned-routes.js +26 -10
- package/dist/memory/project/project-for-root.d.ts +30 -0
- package/dist/memory/project/project-for-root.js +43 -0
- package/dist/memory/project/project-tools.js +10 -5
- package/dist/memory/project/session-root.d.ts +35 -6
- package/dist/memory/project/session-root.js +70 -8
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
- package/dist/portal/bridge/auth-failure-reason.js +20 -3
- package/dist/portal/bridge/bridge.d.ts +6 -1
- package/dist/portal/bridge/bridge.js +36 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/facts/session-replaced.d.ts +19 -0
- package/dist/portal/session/facts/session-replaced.js +22 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -60
- package/dist/portal/session/no-session-next-action.js +3 -2
- package/dist/portal/session/no-session-watch.js +14 -10
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/rebind.d.ts +18 -0
- package/dist/portal/session/rebind.js +38 -0
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +13 -0
- package/dist/portal/session/session-manager.js +42 -3
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +26 -10
- package/dist/portal/session/session.js +48 -49
- package/dist/status-payload.d.ts +12 -1
- package/dist/status-payload.js +4 -1
- package/dist/surface/http-server.js +3 -5
- package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
- package/dist/surface/mcp/faults/mcp-outage.js +20 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
- package/dist/surface/mcp/mcp-proxy.js +23 -12
- package/dist/surface/mcp/mcp.d.ts +10 -1
- package/dist/surface/mcp/mcp.js +59 -29
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
- package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
- package/dist/surface/mcp/proxy/proxy-log.js +7 -22
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/act-preflight.js +6 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/sequence-grade.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +49 -8
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +74 -21
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/coverage-tools.js +5 -2
- package/dist/surface/tools/dynamic-tools.js +43 -46
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +15 -0
- package/dist/surface/tools/explore-tools.js +80 -7
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +50 -3
- package/dist/surface/tools/first-sentence.d.ts +24 -0
- package/dist/surface/tools/first-sentence.js +0 -0
- package/dist/surface/tools/harness-explore.d.ts +125 -1
- package/dist/surface/tools/harness-explore.js +401 -15
- package/dist/surface/tools/harness-plan.d.ts +76 -0
- package/dist/surface/tools/harness-plan.js +156 -0
- package/dist/surface/tools/harness-toolset.js +12 -1
- package/dist/surface/tools/invoke-tool.js +82 -9
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/live-call-text.d.ts +6 -0
- package/dist/surface/tools/live-call-text.js +36 -2
- package/dist/surface/tools/observe-tools.js +69 -15
- package/dist/surface/tools/read/query-windowed.d.ts +1 -1
- package/dist/surface/tools/read/query-windowed.js +1 -1
- package/dist/surface/tools/read-tools.js +1 -2
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +23 -3
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools-helpers.d.ts +8 -7
- package/dist/surface/tools/tools-helpers.js +11 -13
- package/dist/surface/tools/tools.js +70 -30
- package/dist/telemetry/bug-found.js +32 -2
- package/dist/telemetry/feedback-context.js +3 -3
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/harness-drive.d.ts +6 -0
- package/dist/telemetry/harness-drive.js +39 -0
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/mcp-connection.d.ts +14 -1
- package/dist/telemetry/mcp-connection.js +17 -2
- package/dist/telemetry/onboarding-firsts.d.ts +30 -1
- package/dist/telemetry/onboarding-firsts.js +54 -12
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/telemetry/verification-of.js +31 -3
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/agent-cheatsheet.md +7 -3
- package/docs/autodrive.md +42 -2
- package/docs/benchmarks.md +1 -1
- package/docs/capabilities.mdx +1 -1
- package/docs/cli/cloud.mdx +11 -9
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/cli/status.mdx +35 -20
- package/docs/cli.mdx +8 -18
- package/docs/desktop.mdx +1 -1
- package/docs/faq.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +144 -4
- package/docs/system-map.md +6 -4
- package/docs/telemetry.md +2 -1
- package/docs/token-efficiency.md +2 -2
- package/docs/tools/overview.mdx +1 -1
- package/docs/tools/query.mdx +10 -10
- package/docs/troubleshooting.mdx +8 -2
- package/docs/usage.md +2 -2
- package/docs/what-is-recorded.md +62 -0
- package/package.json +5 -5
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
1
|
+
import { basename, join } from 'node:path';
|
|
2
|
+
import { ReticleDir } from '@reticlehq/core';
|
|
2
3
|
import { reticleDirPaths, visualDir } from '../../project/dir/reticle-dir.js';
|
|
4
|
+
import { EVIDENCE_DIRS } from './workspace-tiers.js';
|
|
3
5
|
/** Keep at most this many session journals on disk; older ones are pruned by recency. */
|
|
4
6
|
export const DEFAULT_SESSION_RETENTION = 20;
|
|
5
7
|
/**
|
|
@@ -13,8 +15,70 @@ export const DEFAULT_DIFF_RETENTION = 10;
|
|
|
13
15
|
*/
|
|
14
16
|
export const DEFAULT_FEEDBACK_RETENTION = 20;
|
|
15
17
|
const DIFF_SUFFIX = '.diff.png';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
const PNG_SUFFIX = '.png';
|
|
19
|
+
/**
|
|
20
|
+
* The TOTAL the evidence tier of one `.reticle/` may occupy, across sessions, runs, overlay diffs
|
|
21
|
+
* and feedback copies together.
|
|
22
|
+
*
|
|
23
|
+
* Every bound above this line counts things, and a count cannot see a size: twenty session
|
|
24
|
+
* directories is twenty UNBOUNDED directories. A workspace in the field grew to multiple gigabytes
|
|
25
|
+
* with all three counts being honoured the whole time, because one drive against a chatty app
|
|
26
|
+
* appends response bodies and DOM text for as long as the drive lasts, and nothing was looking at
|
|
27
|
+
* the total.
|
|
28
|
+
*
|
|
29
|
+
* Why this number and not one an order of magnitude either side. An ordinary session journal is
|
|
30
|
+
* hundreds of kilobytes to a few megabytes; a long drive against an app with large JSON responses
|
|
31
|
+
* runs to tens. Twenty of the ordinary ones fit inside this budget outright, which is the working
|
|
32
|
+
* set somebody plausibly wants — the evidence a person reads is from the drive they are looking at
|
|
33
|
+
* and the two before it, and nobody has ever opened the twentieth. A tenth of this (~50 MB) is
|
|
34
|
+
* smaller than a single bad session, so the budget would evict the newest evidence while it was
|
|
35
|
+
* still being written and the count bound could never be reached; ten times it (~5 GB) is not a
|
|
36
|
+
* bound a laptop notices, and 5 GB of untouched journals is the same abandoned-Reticle report with
|
|
37
|
+
* a different number in it. This is roughly one node_modules: large enough to be invisible in a
|
|
38
|
+
* project checkout, small enough that it can never be the reason a disk fills.
|
|
39
|
+
*
|
|
40
|
+
* Both bounds stay, and they are independent on purpose. The count is the cheap guard — it needs one
|
|
41
|
+
* stat per entry and it keeps the directory listing readable. The budget is the guard that holds
|
|
42
|
+
* when the counts are all satisfied and the bytes are not, which is the only situation anybody has
|
|
43
|
+
* actually been hurt by. Either one alone leaves the other's failure unguarded.
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_EVIDENCE_BUDGET_BYTES = 512 * 1024 * 1024;
|
|
46
|
+
/** No session is open. The default everywhere, so an existing caller keeps its old behaviour. */
|
|
47
|
+
const NO_LIVE_SESSIONS = new Set();
|
|
48
|
+
/**
|
|
49
|
+
* Is this entry the journal directory of a session that is STILL BEING WRITTEN?
|
|
50
|
+
*
|
|
51
|
+
* A session directory is named for its session id, so the id IS the basename. Scoped to the
|
|
52
|
+
* sessions tier on purpose: a run artifact or a diff that happens to share a session's name is not
|
|
53
|
+
* a journal, and nothing is holding it open.
|
|
54
|
+
*/
|
|
55
|
+
function isOpenSession(entry, live) {
|
|
56
|
+
return ReticleDir.SESSIONS_SUBDIR === entry.under && live.has(basename(entry.path));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Pure: the paths to remove so the EVIDENCE tier fits in `budgetBytes`. Oldest first, memory-tier
|
|
60
|
+
* entries never selected and never counted. No IO — the call site stats and removes, exactly as
|
|
61
|
+
* `selectPrunable` and `pruneByRecency` already split it.
|
|
62
|
+
*/
|
|
63
|
+
export function selectOverBudget(entries, budgetBytes, live = NO_LIVE_SESSIONS) {
|
|
64
|
+
const evidence = entries.filter((entry) => EVIDENCE_DIRS.includes(entry.under));
|
|
65
|
+
// Live sessions still COUNT — their bytes are on disk and the budget is about the disk. They are
|
|
66
|
+
// only excluded from the eviction list, so a session over budget on its own is left alone rather
|
|
67
|
+
// than deleted under its own writer.
|
|
68
|
+
let total = evidence.reduce((sum, entry) => sum + entry.sizeBytes, 0);
|
|
69
|
+
if (total <= budgetBytes)
|
|
70
|
+
return [];
|
|
71
|
+
const doomed = [];
|
|
72
|
+
for (const entry of [...evidence].sort((a, b) => a.mtimeMs - b.mtimeMs)) {
|
|
73
|
+
if (isOpenSession(entry, live))
|
|
74
|
+
continue;
|
|
75
|
+
if (total <= budgetBytes)
|
|
76
|
+
break;
|
|
77
|
+
doomed.push(entry.path);
|
|
78
|
+
total -= entry.sizeBytes;
|
|
79
|
+
}
|
|
80
|
+
return doomed;
|
|
81
|
+
}
|
|
18
82
|
/**
|
|
19
83
|
* Pure: given session dirs with their mtimes, the names to remove so only the `retention` most-recent
|
|
20
84
|
* remain. Oldest-first. Exported for testing (no IO).
|
|
@@ -71,10 +135,23 @@ async function pruneByRecency(fs, dir, retention, keep = () => true) {
|
|
|
71
135
|
* Bound the journal on disk: keep the `retention` most-recent session directories under
|
|
72
136
|
* `.reticle/sessions/`, remove the rest by mtime. Never throws — retention is best-effort maintenance,
|
|
73
137
|
* so a stat/rm failure on one dir (or a missing sessions/ dir) is swallowed, never crashing a session.
|
|
74
|
-
* Mirrors RunStore's amortized pruning; run on daemon start.
|
|
138
|
+
* Mirrors RunStore's amortized pruning; run on daemon start and at the end of every session.
|
|
139
|
+
*
|
|
140
|
+
* The COUNT bound only. This used to sweep the tier total as well, from when it was the single
|
|
141
|
+
* sweep both call sites used; `pruneWorkspace` is that single place now and runs the budget last,
|
|
142
|
+
* on purpose, so the leftover call here was a second walk of every evidence entry on every teardown
|
|
143
|
+
* — and it ran FIRST, sizing everything the counts were about to delete anyway.
|
|
144
|
+
*
|
|
145
|
+
* An open session is excluded from BOTH bounds — see `RetentionOptions.live`.
|
|
146
|
+
*
|
|
147
|
+
* ponytail: excluded via `keep`, so an open session is not COUNTED toward the retention cap either.
|
|
148
|
+
* The directory can therefore hold `retention` + (concurrently open sessions) entries, which is a
|
|
149
|
+
* handful. Count them separately only if somebody runs enough parallel sessions for it to matter.
|
|
75
150
|
*/
|
|
76
|
-
export async function pruneSessions(fs, root,
|
|
77
|
-
|
|
151
|
+
export async function pruneSessions(fs, root, options = {}) {
|
|
152
|
+
const live = options.live ?? NO_LIVE_SESSIONS;
|
|
153
|
+
const retention = options.retention ?? DEFAULT_SESSION_RETENTION;
|
|
154
|
+
await pruneByRecency(fs, reticleDirPaths(root).sessions, retention, (name) => !live.has(name));
|
|
78
155
|
}
|
|
79
156
|
/**
|
|
80
157
|
* Bound `.reticle/visual/` — the largest thing in the workspace, and the only one with no delete
|
|
@@ -91,20 +168,113 @@ export async function pruneSessions(fs, root, retention = DEFAULT_SESSION_RETENT
|
|
|
91
168
|
* `pruneSessions`: this is maintenance, and maintenance must never be the reason a daemon fails.
|
|
92
169
|
*/
|
|
93
170
|
export async function pruneVisualDiffs(fs, root, retention = DEFAULT_DIFF_RETENTION) {
|
|
171
|
+
for (const dir of await visualDirs(fs, root)) {
|
|
172
|
+
await pruneByRecency(fs, dir, retention, isDiff);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** `visual/` plus its per-runtime subdirectories. Empty when nothing has been captured yet. */
|
|
176
|
+
async function visualDirs(fs, root) {
|
|
94
177
|
const top = visualDir(root);
|
|
95
178
|
const dirs = [top];
|
|
96
179
|
try {
|
|
97
180
|
// A per-runtime subdirectory is anything here that is not itself a PNG.
|
|
98
181
|
for (const entry of await fs.readdir(top)) {
|
|
99
|
-
if (!entry.endsWith(
|
|
182
|
+
if (!entry.endsWith(PNG_SUFFIX))
|
|
100
183
|
dirs.push(join(top, entry));
|
|
101
184
|
}
|
|
102
185
|
}
|
|
103
186
|
catch {
|
|
104
|
-
return;
|
|
187
|
+
return [];
|
|
105
188
|
}
|
|
106
|
-
|
|
107
|
-
|
|
189
|
+
return dirs;
|
|
190
|
+
}
|
|
191
|
+
const isDiff = (name) => name.endsWith(DIFF_SUFFIX);
|
|
192
|
+
/**
|
|
193
|
+
* Bytes under `path`, summed recursively. A session journal is a DIRECTORY, and `stat` on one
|
|
194
|
+
* reports the dirent, not the megabytes of JSONL inside it.
|
|
195
|
+
*
|
|
196
|
+
* readdir-first because the port exposes no `isDirectory`: a readdir that fails is a file, and its
|
|
197
|
+
* own stat answers. Never throws — an unreadable entry contributes nothing, which can only make the
|
|
198
|
+
* sweep evict less than it should, never more.
|
|
199
|
+
*/
|
|
200
|
+
async function sizeOf(fs, path) {
|
|
201
|
+
let names;
|
|
202
|
+
try {
|
|
203
|
+
names = await fs.readdir(path);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
try {
|
|
207
|
+
return (await fs.stat(path)).size;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let total = 0;
|
|
214
|
+
for (const name of names)
|
|
215
|
+
total += await sizeOf(fs, join(path, name));
|
|
216
|
+
return total;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Every evidence entry in this `.reticle/`, sized and dated. The IO half of the byte budget.
|
|
220
|
+
*
|
|
221
|
+
* `visual/` is the one evidence directory holding something that is not evidence — the PNG
|
|
222
|
+
* BASELINES a comparison means anything against — so the same `.diff.png` filter `pruneVisualDiffs`
|
|
223
|
+
* applies is applied here. The tier list is the coarse rule; that filter is the finer one, and
|
|
224
|
+
* deleting a baseline to save disk turns the next run's verdict into `baseline-missing`.
|
|
225
|
+
*/
|
|
226
|
+
async function collectEvidence(fs, root) {
|
|
227
|
+
const entries = [];
|
|
228
|
+
for (const under of EVIDENCE_DIRS) {
|
|
229
|
+
const isVisual = ReticleDir.VISUAL_SUBDIR === under;
|
|
230
|
+
const dirs = isVisual ? await visualDirs(fs, root) : [join(root, under)];
|
|
231
|
+
for (const dir of dirs) {
|
|
232
|
+
let names;
|
|
233
|
+
try {
|
|
234
|
+
names = await fs.readdir(dir);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
continue; // nothing written here yet
|
|
238
|
+
}
|
|
239
|
+
for (const name of names) {
|
|
240
|
+
if (isVisual && !isDiff(name))
|
|
241
|
+
continue;
|
|
242
|
+
const path = join(dir, name);
|
|
243
|
+
try {
|
|
244
|
+
const { mtimeMs } = await fs.stat(path);
|
|
245
|
+
entries.push({ path, under, sizeBytes: await sizeOf(fs, path), mtimeMs });
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
/* unreadable entry — skip */
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return entries;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Hold the evidence tier of one `.reticle/` under a TOTAL byte budget, evicting oldest-first.
|
|
257
|
+
*
|
|
258
|
+
* The memory tier is not read, not counted and not touched: the maintainer's constraint is a cap
|
|
259
|
+
* that never stops memory and flows being created, so a full disk costs somebody their session
|
|
260
|
+
* journals and never their flows.
|
|
261
|
+
*
|
|
262
|
+
* Never throws, like everything else here. Retention is maintenance, and maintenance must never be
|
|
263
|
+
* the reason a daemon fails to come up or a session dies.
|
|
264
|
+
*/
|
|
265
|
+
export async function pruneEvidenceBudget(fs, root, budgetBytes = DEFAULT_EVIDENCE_BUDGET_BYTES, live = NO_LIVE_SESSIONS) {
|
|
266
|
+
try {
|
|
267
|
+
for (const path of selectOverBudget(await collectEvidence(fs, root), budgetBytes, live)) {
|
|
268
|
+
try {
|
|
269
|
+
await fs.rm(path);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
/* best-effort */
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
/* maintenance never fails a daemon */
|
|
108
278
|
}
|
|
109
279
|
}
|
|
110
280
|
/**
|
|
@@ -118,5 +288,5 @@ export async function pruneVisualDiffs(fs, root, retention = DEFAULT_DIFF_RETENT
|
|
|
118
288
|
* retention rule is the same rule whether the directory holds journals, diffs or reports.
|
|
119
289
|
*/
|
|
120
290
|
export async function pruneFeedback(fs, root, retention = DEFAULT_FEEDBACK_RETENTION) {
|
|
121
|
-
await pruneByRecency(fs, join(root, FEEDBACK_SUBDIR), retention);
|
|
291
|
+
await pruneByRecency(fs, join(root, ReticleDir.FEEDBACK_SUBDIR), retention);
|
|
122
292
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything Reticle prunes in ONE `.reticle/` workspace, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Called from two moments and both matter: daemon start (the daemon's own tree) and session end
|
|
5
|
+
* (the tree that SESSION wrote into). Those are usually different directories — a globally
|
|
6
|
+
* registered daemon stands in `$HOME` — and the per-project one is where the bytes actually
|
|
7
|
+
* accumulate, so sweeping only the first swept the wrong tree for the common case.
|
|
8
|
+
*
|
|
9
|
+
* The four bounds were inlined at the call site and one of them was simply missing:
|
|
10
|
+
* `pruneEvidenceBudget` had been written, tested, and never called by anything. The three COUNT
|
|
11
|
+
* bounds were wired and the BYTE bound was not, which is the combination the budget's own header
|
|
12
|
+
* warns about -- "twenty session directories is twenty UNBOUNDED directories", beside a field
|
|
13
|
+
* report of a multi-gigabyte `.reticle/sessions` with all three counts honoured throughout.
|
|
14
|
+
*
|
|
15
|
+
* Counts and bytes answer different questions and neither substitutes for the other. A count says
|
|
16
|
+
* how MANY sessions are kept; it says nothing about how large one is allowed to get. A chatty app
|
|
17
|
+
* writes one enormous journal and never trips a count.
|
|
18
|
+
*
|
|
19
|
+
* Gathered into a function rather than left as four statements so that "what maintenance runs at
|
|
20
|
+
* startup" has one answer that can be TESTED. Inlined, the only way to check the budget was wired
|
|
21
|
+
* was to grep the call site for its name -- and a grep passes on a comment that quotes the code,
|
|
22
|
+
* which has already happened in this repository.
|
|
23
|
+
*
|
|
24
|
+
* Order: counts first, budget last. The counts are cheap and bounded; the budget walks and sizes
|
|
25
|
+
* what the counts leave behind, so running it second means it sizes less.
|
|
26
|
+
*
|
|
27
|
+
* NEVER THROWS. Maintenance must not be the reason a daemon fails to come up, and each prune below
|
|
28
|
+
* already swallows its own errors -- this only has to avoid adding a new way to fail.
|
|
29
|
+
*/
|
|
30
|
+
import type { FileSystemPort } from '../../project/fs/fs-port.js';
|
|
31
|
+
import { type RetainPolicy } from './retain-policy.js';
|
|
32
|
+
/**
|
|
33
|
+
* What this sweep is allowed to keep. Every field optional, defaulting to the built-in bound.
|
|
34
|
+
*
|
|
35
|
+
* Partial rather than a whole `RetainPolicy` for two reasons that happen to agree: a test states
|
|
36
|
+
* only the bound it is exercising instead of writing half a gigabyte to disk, and a project's
|
|
37
|
+
* `.reticle.json` states only the bounds it cares about. Same reason the clock is injected
|
|
38
|
+
* everywhere else here — a bound that can only be exercised by producing the real quantity is a
|
|
39
|
+
* bound nothing exercises.
|
|
40
|
+
*/
|
|
41
|
+
export type PruneWorkspaceOptions = Partial<RetainPolicy>;
|
|
42
|
+
export declare function pruneWorkspace(fs: FileSystemPort, root: string, live: ReadonlySet<string>, options?: PruneWorkspaceOptions): Promise<void>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { pruneEvidenceBudget, pruneFeedback, pruneSessions, pruneVisualDiffs, } from './retention.js';
|
|
2
|
+
import { DEFAULT_RETAIN } from './retain-policy.js';
|
|
3
|
+
export async function pruneWorkspace(fs, root, live, options = {}) {
|
|
4
|
+
const retain = { ...DEFAULT_RETAIN, ...options };
|
|
5
|
+
// Empty in the ordinary case (nothing has connected yet); this path also runs on a daemon that is
|
|
6
|
+
// already serving sessions, which is why `live` is threaded rather than assumed empty.
|
|
7
|
+
await pruneSessions(fs, root, { live, retention: retain.sessions });
|
|
8
|
+
// The largest thing in the workspace, and for a long time the only one with no delete path.
|
|
9
|
+
await pruneVisualDiffs(fs, root, retain.visual);
|
|
10
|
+
// Write-only local copies of reports the outbox already carries.
|
|
11
|
+
await pruneFeedback(fs, root, retain.feedback);
|
|
12
|
+
// LAST, and the one that was missing: the only bound that limits SIZE rather than count.
|
|
13
|
+
await pruneEvidenceBudget(fs, root, retain.budgetBytes, live);
|
|
14
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { FileSystemPort } from '../../project/fs/fs-port.js';
|
|
2
|
-
/** Every name Reticle treats as machine-local, however it is spelled in the ignore file. */
|
|
3
|
-
export declare const TRANSIENT_NAMES: readonly string[];
|
|
4
2
|
/**
|
|
5
3
|
* Write `.reticle/.gitignore` if it is not already there.
|
|
6
4
|
*
|
|
@@ -1,53 +1,21 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { ReticleDir } from '@reticlehq/core';
|
|
3
|
+
import { LOCAL_DIRS, LOCAL_FILES } from './workspace-tiers.js';
|
|
3
4
|
/**
|
|
4
5
|
* The `.reticle/` entries that are LOCAL state, and must never be committed by accident.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Read from the tier table rather than restated here: which half a name belongs to is one decision,
|
|
8
|
+
* and a second copy of it is the list that silently disagrees. This file asks the table exactly one
|
|
9
|
+
* of its two questions — `sharing` — and has no opinion at all about what retention may delete.
|
|
9
10
|
*
|
|
10
11
|
* The reason this matters beyond tidiness: a session journal carries URLs, request and response
|
|
11
12
|
* bodies, and DOM text from the app under test. `.reticle/` is created by the daemon rather than by
|
|
12
13
|
* the user, so it arrives untracked and unexplained, and one `git add -A` puts an app's traffic into
|
|
13
14
|
* a shared repository. Reticle created the directory; the ignore for it is Reticle's to write.
|
|
14
15
|
*/
|
|
15
|
-
/**
|
|
16
|
-
* The local feedback copies — `.reticle/feedback/`.
|
|
17
|
-
*
|
|
18
|
-
* Named here rather than imported: the directory is spelled inline where it is written, in files
|
|
19
|
-
* another agent owns, and the ignore for a directory Reticle creates is Reticle's to write whether
|
|
20
|
-
* or not that name has been hoisted yet.
|
|
21
|
-
*/
|
|
22
|
-
const FEEDBACK_SUBDIR = 'feedback';
|
|
23
|
-
/** Local directories, without the trailing slash the ignore file wants. */
|
|
24
|
-
const TRANSIENT_DIRS = [
|
|
25
|
-
ReticleDir.SESSIONS_SUBDIR,
|
|
26
|
-
ReticleDir.RUNS_SUBDIR,
|
|
27
|
-
ReticleDir.VISUAL_SUBDIR,
|
|
28
|
-
// Write-only: a local copy of what was already sent. The outbox is the record.
|
|
29
|
-
FEEDBACK_SUBDIR,
|
|
30
|
-
];
|
|
31
|
-
/** Local files. Exported with the directories as `TRANSIENT_NAMES`, which the partition guard reads. */
|
|
32
|
-
const TRANSIENT_FILES = [
|
|
33
|
-
ReticleDir.PROJECT_FILE,
|
|
34
|
-
ReticleDir.AMBIENT_FILE,
|
|
35
|
-
ReticleDir.ENVELOPES_FILE,
|
|
36
|
-
ReticleDir.FLAKE_FILE,
|
|
37
|
-
ReticleDir.TIERS_FILE,
|
|
38
|
-
// The user's own record of what Reticle did for them, on THIS machine.
|
|
39
|
-
ReticleDir.IMPACT_FILE,
|
|
40
|
-
// This machine's conversation with the server. Its own doc comment says why committing it is
|
|
41
|
-
// harmful: one machine's pull cursor makes every other machine skip what it has not seen.
|
|
42
|
-
ReticleDir.CLOUD_STATE_FILE,
|
|
43
|
-
// Triage pulled back from the dashboard — a cache of somebody's decisions, re-pullable at will.
|
|
44
|
-
ReticleDir.ISSUES_FILE,
|
|
45
|
-
];
|
|
46
|
-
/** Every name Reticle treats as machine-local, however it is spelled in the ignore file. */
|
|
47
|
-
export const TRANSIENT_NAMES = [...TRANSIENT_DIRS, ...TRANSIENT_FILES];
|
|
48
16
|
const TRANSIENT = [
|
|
49
|
-
...
|
|
50
|
-
...
|
|
17
|
+
...LOCAL_DIRS.map((dir) => `${dir}/`),
|
|
18
|
+
...LOCAL_FILES,
|
|
51
19
|
'*.log',
|
|
52
20
|
'*.tmp',
|
|
53
21
|
];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* May retention delete this to reclaim disk?
|
|
3
|
+
*
|
|
4
|
+
* MEMORY is why `.reticle/` exists — what a flow replays from, what a deviation is measured
|
|
5
|
+
* against. It is small, it is never evicted, and it must keep being written however full the disk
|
|
6
|
+
* is. EVIDENCE is what one drive left behind: journals, run artifacts, pixel diffs. It is large, it
|
|
7
|
+
* ages out, and losing the oldest of it costs nothing anybody can name.
|
|
8
|
+
*/
|
|
9
|
+
export declare const Durability: {
|
|
10
|
+
readonly MEMORY: "memory";
|
|
11
|
+
readonly EVIDENCE: "evidence";
|
|
12
|
+
};
|
|
13
|
+
export type Durability = (typeof Durability)[keyof typeof Durability];
|
|
14
|
+
/**
|
|
15
|
+
* Does this belong in the repository, or only on the machine that wrote it?
|
|
16
|
+
*
|
|
17
|
+
* COMMIT is the shareable half: a flow nobody else can replay is not a regression check. LOCAL is
|
|
18
|
+
* per-machine churn, and journals carry URLs, request and response bodies and page text from the
|
|
19
|
+
* app under test, so keeping them out of a shared repository is worth doing on its own account.
|
|
20
|
+
*/
|
|
21
|
+
export declare const Sharing: {
|
|
22
|
+
readonly COMMIT: "commit";
|
|
23
|
+
readonly LOCAL: "local";
|
|
24
|
+
};
|
|
25
|
+
export type Sharing = (typeof Sharing)[keyof typeof Sharing];
|
|
26
|
+
/** What one `.reticle/` entry is, on both axes at once. */
|
|
27
|
+
export interface WorkspaceTier {
|
|
28
|
+
readonly durability: Durability;
|
|
29
|
+
readonly sharing: Sharing;
|
|
30
|
+
/** A directory is ignored as `name/` and swept entry by entry; a file is neither. */
|
|
31
|
+
readonly isDirectory: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Every top-level name `.reticle/` can hold, classified on both axes.
|
|
35
|
+
*
|
|
36
|
+
* One list used to answer both questions with one answer, and they are not the same question.
|
|
37
|
+
* `capsules/` is committed — a fail-to-pass capsule a teammate cannot replay is not a regression
|
|
38
|
+
* check — AND it is evidence a drive left behind, which grows without bound. That cell did not
|
|
39
|
+
* exist, so the byte budget, which read the gitignore's list, structurally could not see capsules
|
|
40
|
+
* however large they got. Splitting the axes is what makes that sayable; it does not by itself say
|
|
41
|
+
* it, and `capsules/` stays MEMORY here until the budget is measured against it.
|
|
42
|
+
*
|
|
43
|
+
* Declaration order is the order the ignore file lists them, so the file a user already has on disk
|
|
44
|
+
* and the one written today are the same file.
|
|
45
|
+
*/
|
|
46
|
+
export declare const WORKSPACE_TIERS: Readonly<Record<string, WorkspaceTier>>;
|
|
47
|
+
/** Machine-local directories, without the trailing slash the ignore file wants. */
|
|
48
|
+
export declare const LOCAL_DIRS: readonly string[];
|
|
49
|
+
/** Machine-local files. */
|
|
50
|
+
export declare const LOCAL_FILES: readonly string[];
|
|
51
|
+
/** The directories retention sweeps, ignored or not. The byte budget and the count bounds read this. */
|
|
52
|
+
export declare const EVIDENCE_DIRS: readonly string[];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ReticleDir } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* May retention delete this to reclaim disk?
|
|
4
|
+
*
|
|
5
|
+
* MEMORY is why `.reticle/` exists — what a flow replays from, what a deviation is measured
|
|
6
|
+
* against. It is small, it is never evicted, and it must keep being written however full the disk
|
|
7
|
+
* is. EVIDENCE is what one drive left behind: journals, run artifacts, pixel diffs. It is large, it
|
|
8
|
+
* ages out, and losing the oldest of it costs nothing anybody can name.
|
|
9
|
+
*/
|
|
10
|
+
export const Durability = {
|
|
11
|
+
MEMORY: 'memory',
|
|
12
|
+
EVIDENCE: 'evidence',
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Does this belong in the repository, or only on the machine that wrote it?
|
|
16
|
+
*
|
|
17
|
+
* COMMIT is the shareable half: a flow nobody else can replay is not a regression check. LOCAL is
|
|
18
|
+
* per-machine churn, and journals carry URLs, request and response bodies and page text from the
|
|
19
|
+
* app under test, so keeping them out of a shared repository is worth doing on its own account.
|
|
20
|
+
*/
|
|
21
|
+
export const Sharing = {
|
|
22
|
+
COMMIT: 'commit',
|
|
23
|
+
LOCAL: 'local',
|
|
24
|
+
};
|
|
25
|
+
const localDir = { durability: Durability.EVIDENCE, sharing: Sharing.LOCAL, isDirectory: true };
|
|
26
|
+
const localFile = { durability: Durability.MEMORY, sharing: Sharing.LOCAL, isDirectory: false };
|
|
27
|
+
const sharedDir = { durability: Durability.MEMORY, sharing: Sharing.COMMIT, isDirectory: true };
|
|
28
|
+
const sharedFile = { durability: Durability.MEMORY, sharing: Sharing.COMMIT, isDirectory: false };
|
|
29
|
+
/**
|
|
30
|
+
* Every top-level name `.reticle/` can hold, classified on both axes.
|
|
31
|
+
*
|
|
32
|
+
* One list used to answer both questions with one answer, and they are not the same question.
|
|
33
|
+
* `capsules/` is committed — a fail-to-pass capsule a teammate cannot replay is not a regression
|
|
34
|
+
* check — AND it is evidence a drive left behind, which grows without bound. That cell did not
|
|
35
|
+
* exist, so the byte budget, which read the gitignore's list, structurally could not see capsules
|
|
36
|
+
* however large they got. Splitting the axes is what makes that sayable; it does not by itself say
|
|
37
|
+
* it, and `capsules/` stays MEMORY here until the budget is measured against it.
|
|
38
|
+
*
|
|
39
|
+
* Declaration order is the order the ignore file lists them, so the file a user already has on disk
|
|
40
|
+
* and the one written today are the same file.
|
|
41
|
+
*/
|
|
42
|
+
export const WORKSPACE_TIERS = {
|
|
43
|
+
[ReticleDir.SESSIONS_SUBDIR]: localDir,
|
|
44
|
+
[ReticleDir.RUNS_SUBDIR]: localDir,
|
|
45
|
+
[ReticleDir.VISUAL_SUBDIR]: localDir,
|
|
46
|
+
// Write-only: a local copy of what was already sent. The outbox is the record.
|
|
47
|
+
[ReticleDir.FEEDBACK_SUBDIR]: localDir,
|
|
48
|
+
[ReticleDir.PROJECT_FILE]: localFile,
|
|
49
|
+
[ReticleDir.AMBIENT_FILE]: localFile,
|
|
50
|
+
[ReticleDir.ENVELOPES_FILE]: localFile,
|
|
51
|
+
[ReticleDir.FLAKE_FILE]: localFile,
|
|
52
|
+
[ReticleDir.TIERS_FILE]: localFile,
|
|
53
|
+
// The user's own record of what Reticle did for them, on THIS machine.
|
|
54
|
+
[ReticleDir.IMPACT_FILE]: localFile,
|
|
55
|
+
// This machine's conversation with the server. Its own doc comment says why committing it is
|
|
56
|
+
// harmful: one machine's pull cursor makes every other machine skip what it has not seen.
|
|
57
|
+
[ReticleDir.CLOUD_STATE_FILE]: localFile,
|
|
58
|
+
// Triage pulled back from the dashboard — a cache of somebody's decisions, re-pullable at will.
|
|
59
|
+
[ReticleDir.ISSUES_FILE]: localFile,
|
|
60
|
+
[ReticleDir.CONTRACT_FILE]: sharedFile,
|
|
61
|
+
[ReticleDir.FLOWS_SUBDIR]: sharedDir,
|
|
62
|
+
[ReticleDir.BASELINES_SUBDIR]: sharedDir,
|
|
63
|
+
[ReticleDir.CAPSULES_SUBDIR]: sharedDir,
|
|
64
|
+
[ReticleDir.INTENT_FILE]: sharedFile,
|
|
65
|
+
// The sharded form of the same ledger, and shared for the same reason.
|
|
66
|
+
[ReticleDir.INTENT_SUBDIR]: sharedDir,
|
|
67
|
+
[ReticleDir.CLOUD_LINK_FILE]: sharedFile,
|
|
68
|
+
// A hook is a decision the whole team shares, exactly like a package script: one that exists only
|
|
69
|
+
// on the machine that wrote it is a rule nobody else is following. It names COMMANDS and never a
|
|
70
|
+
// credential, so it is safe to commit — the same reasoning that puts `cloud.json` here while its
|
|
71
|
+
// API key stays in ~/.reticle.
|
|
72
|
+
[ReticleDir.HOOKS_FILE]: sharedFile,
|
|
73
|
+
// What a drive types into each labelled field. Committed for two reasons: a replay must send
|
|
74
|
+
// exactly what the recording sent or it is not a replay, and a value a model wrote once should be
|
|
75
|
+
// reviewable and editable by the team rather than regenerated differently on every machine.
|
|
76
|
+
[ReticleDir.FILL_VALUES_FILE]: sharedFile,
|
|
77
|
+
};
|
|
78
|
+
function namesWhere(match) {
|
|
79
|
+
return Object.entries(WORKSPACE_TIERS)
|
|
80
|
+
.filter(([, tier]) => match(tier))
|
|
81
|
+
.map(([name]) => name);
|
|
82
|
+
}
|
|
83
|
+
/** Machine-local directories, without the trailing slash the ignore file wants. */
|
|
84
|
+
export const LOCAL_DIRS = namesWhere((tier) => Sharing.LOCAL === tier.sharing && tier.isDirectory);
|
|
85
|
+
/** Machine-local files. */
|
|
86
|
+
export const LOCAL_FILES = namesWhere((tier) => Sharing.LOCAL === tier.sharing && !tier.isDirectory);
|
|
87
|
+
/** The directories retention sweeps, ignored or not. The byte budget and the count bounds read this. */
|
|
88
|
+
export const EVIDENCE_DIRS = namesWhere((tier) => Durability.EVIDENCE === tier.durability && tier.isDirectory);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key an envelope's statistics accumulate under.
|
|
3
|
+
*
|
|
4
|
+
* The envelope map was keyed on the raw `pathname`. On any app whose URLs carry ids — most of them —
|
|
5
|
+
* `/orders/1001` and `/orders/1002` are different keys, so every visit mints a fresh envelope
|
|
6
|
+
* holding one sample.
|
|
7
|
+
*
|
|
8
|
+
* The cost is not the file size. `deviation-report` skips any envelope below
|
|
9
|
+
* `MIN_ENVELOPE_SAMPLES`, so on such an app no envelope ever qualifies, every report answers
|
|
10
|
+
* "envelope too new", and the deviation feature never turns on at all — silently, permanently, with
|
|
11
|
+
* nothing anywhere saying why. The unbounded file and the dead feature are one defect.
|
|
12
|
+
*
|
|
13
|
+
* CONSERVATIVE on purpose. Over-collapsing is the worse error: merging two genuinely different
|
|
14
|
+
* routes builds a baseline from two different pages, and a confident comparison against the wrong
|
|
15
|
+
* population is more expensive than no comparison. So a segment is an id only when it could not
|
|
16
|
+
* plausibly be a route NAME.
|
|
17
|
+
*
|
|
18
|
+
* This is the statistics key alone. The observed path is untouched, because a report has to be able
|
|
19
|
+
* to say "/orders/1001 was slow" — the template tells you where to look, the path tells you what to
|
|
20
|
+
* open.
|
|
21
|
+
*/
|
|
22
|
+
export declare function routeTemplate(pathname: string): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key an envelope's statistics accumulate under.
|
|
3
|
+
*
|
|
4
|
+
* The envelope map was keyed on the raw `pathname`. On any app whose URLs carry ids — most of them —
|
|
5
|
+
* `/orders/1001` and `/orders/1002` are different keys, so every visit mints a fresh envelope
|
|
6
|
+
* holding one sample.
|
|
7
|
+
*
|
|
8
|
+
* The cost is not the file size. `deviation-report` skips any envelope below
|
|
9
|
+
* `MIN_ENVELOPE_SAMPLES`, so on such an app no envelope ever qualifies, every report answers
|
|
10
|
+
* "envelope too new", and the deviation feature never turns on at all — silently, permanently, with
|
|
11
|
+
* nothing anywhere saying why. The unbounded file and the dead feature are one defect.
|
|
12
|
+
*
|
|
13
|
+
* CONSERVATIVE on purpose. Over-collapsing is the worse error: merging two genuinely different
|
|
14
|
+
* routes builds a baseline from two different pages, and a confident comparison against the wrong
|
|
15
|
+
* population is more expensive than no comparison. So a segment is an id only when it could not
|
|
16
|
+
* plausibly be a route NAME.
|
|
17
|
+
*
|
|
18
|
+
* This is the statistics key alone. The observed path is untouched, because a report has to be able
|
|
19
|
+
* to say "/orders/1001 was slow" — the template tells you where to look, the path tells you what to
|
|
20
|
+
* open.
|
|
21
|
+
*/
|
|
22
|
+
/** Stands in for any id segment. Not a valid path segment itself, so it cannot collide with one. */
|
|
23
|
+
const ID = ':id';
|
|
24
|
+
/** 8+ hex characters: a token, a hash, a short uuid. Below that it is as likely to be a word. */
|
|
25
|
+
const LONG_HEX = /^[0-9a-f]{8,}$/i;
|
|
26
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
27
|
+
const ALL_DIGITS = /^\d+$/;
|
|
28
|
+
/**
|
|
29
|
+
* A year collapses too, and that is a deliberate trade rather than an oversight: `/reports/2024`
|
|
30
|
+
* and `/reports/2025` are the same page with different data, which is exactly what one envelope
|
|
31
|
+
* should describe. Distinguishing a year from an id needs to know the app, and guessing would
|
|
32
|
+
* reintroduce the split this exists to remove.
|
|
33
|
+
*/
|
|
34
|
+
function isIdSegment(segment) {
|
|
35
|
+
return ALL_DIGITS.test(segment) || UUID.test(segment) || LONG_HEX.test(segment);
|
|
36
|
+
}
|
|
37
|
+
export function routeTemplate(pathname) {
|
|
38
|
+
if (0 === pathname.length)
|
|
39
|
+
return pathname;
|
|
40
|
+
return pathname
|
|
41
|
+
.split('/')
|
|
42
|
+
.map((segment) => (isIdSegment(segment) ? ID : segment))
|
|
43
|
+
.join('/');
|
|
44
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { EventType, urlForMatch } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* A request the page made for Reticle's OWN code, which the journal declines to keep.
|
|
4
|
+
*
|
|
5
|
+
* Measured over this repo's twenty recorded sessions: `net.request` is 64.6% of every journal byte
|
|
6
|
+
* and `net.detail` a further 17.6%, and the bulk of that is the browser fetching the SDK's module
|
|
7
|
+
* graph over and over as the dev server hot-reloads. Those bytes are Reticle watching itself arrive.
|
|
8
|
+
* They can never be evidence about the app under test, because the app did not ask for them — the
|
|
9
|
+
* instrumentation did, and it did so before the app had done anything worth verifying.
|
|
10
|
+
*
|
|
11
|
+
* The DOM side has drawn this line for a long time (`isReticleUi`), and the transport side draws it
|
|
12
|
+
* on the bridge socket's own path. This is the same line on the network channel.
|
|
13
|
+
*
|
|
14
|
+
* Identity, not resemblance. A request qualifies only when its PATH names our npm scope, which is
|
|
15
|
+
* the one thing about a URL that nobody else can be: `@reticlehq` is a registry scope we own, so a
|
|
16
|
+
* path segment under it is a file from one of our packages however it is being served — from
|
|
17
|
+
* `node_modules`, from a CDN, or through Vite's dep optimizer, which flattens the scope to
|
|
18
|
+
* `@reticlehq_browser.js`. A user's own file is never addressed that way.
|
|
19
|
+
*
|
|
20
|
+
* What this deliberately does NOT do is guess from shape. `/core/dist/index.js` and
|
|
21
|
+
* `/adapters/realm/browser/dist/index.js` are what this repo's own linked checkout serves, and
|
|
22
|
+
* matching those would collapse most of the bytes measured above — but they are ordinary paths that
|
|
23
|
+
* somebody else's app is entitled to serve, and a filter that drops a user's request to save our own
|
|
24
|
+
* bytes is the trade this project has already measured the cost of. So a source-linked checkout of
|
|
25
|
+
* Reticle itself keeps journaling its own SDK, and that is the correct side to be wrong on.
|
|
26
|
+
*
|
|
27
|
+
* Only the query string can carry a scope-shaped string that is really app data (a search for
|
|
28
|
+
* `@reticlehq/browser`), so the decision is taken on `pathname` alone.
|
|
29
|
+
*/
|
|
30
|
+
/** The npm scope directory, as it appears when the package is served from a tree. */
|
|
31
|
+
const SCOPE_PATH = '/@reticlehq/';
|
|
32
|
+
/**
|
|
33
|
+
* The same scope after Vite's dependency optimizer flattens `@scope/name` to one filename. Anchored
|
|
34
|
+
* at a segment boundary so it cannot match in the middle of somebody's filename.
|
|
35
|
+
*/
|
|
36
|
+
const SCOPE_FLATTENED = '/@reticlehq_';
|
|
37
|
+
/**
|
|
38
|
+
* Every event kind that carries a request URL — all of them, on purpose.
|
|
39
|
+
*
|
|
40
|
+
* `reconcileNet` pairs a NET_PENDING with its NET_REQUEST by id and reports an unpaired pending as a
|
|
41
|
+
* HUNG request. Dropping one half of a pair would therefore manufacture a finding out of a request
|
|
42
|
+
* that completed perfectly. The rule is a pure function of the URL, so both halves of a pair always
|
|
43
|
+
* get the same answer and no pair can be split.
|
|
44
|
+
*/
|
|
45
|
+
const NET_TYPES = new Set([
|
|
46
|
+
EventType.NET_REQUEST,
|
|
47
|
+
EventType.NET_PENDING,
|
|
48
|
+
EventType.NET_DETAIL,
|
|
49
|
+
EventType.NET_STREAM,
|
|
50
|
+
]);
|
|
51
|
+
/** A base for parsing relative URLs. Never dereferenced — only the path is read. */
|
|
52
|
+
const RELATIVE_BASE = 'http://reticle.invalid';
|
|
53
|
+
function pathOf(url) {
|
|
54
|
+
try {
|
|
55
|
+
return new URL(url, RELATIVE_BASE).pathname;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return url;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** True when this event is the page fetching Reticle's own code rather than doing anything. */
|
|
62
|
+
export function isSelfObservation(event) {
|
|
63
|
+
if (!NET_TYPES.has(event.type))
|
|
64
|
+
return false;
|
|
65
|
+
const path = pathOf(urlForMatch(event.data));
|
|
66
|
+
return path.includes(SCOPE_PATH) || path.includes(SCOPE_FLATTENED);
|
|
67
|
+
}
|