@reticlehq/server 3.7.0 → 3.8.1
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 +59 -68
- package/bin/reticle.js +9 -2
- package/dist/command/cli/adhoc-suite.js +1 -1
- package/dist/command/cli/adhoc-verdict.d.ts +12 -0
- package/dist/command/cli/adhoc-verdict.js +62 -4
- package/dist/command/cli/answers/gate-hook-message.d.ts +2 -0
- package/dist/command/cli/answers/gate-hook-message.js +5 -1
- package/dist/command/cli/auth/cloud-session.d.ts +11 -19
- package/dist/command/cli/auth/cloud-session.js +23 -0
- package/dist/command/cli/auth/device-flow.d.ts +3 -37
- package/dist/command/cli/cli-doctor.js +66 -21
- package/dist/command/cli/cli-flow-commands.js +53 -11
- package/dist/command/cli/cli-kill.d.ts +8 -0
- package/dist/command/cli/cli-kill.js +43 -3
- package/dist/command/cli/cli-parse-feedback.d.ts +4 -0
- package/dist/command/cli/cli-parse-feedback.js +13 -1
- package/dist/command/cli/cli-parse-grammar.d.ts +1 -2
- package/dist/command/cli/cli-parse-grammar.js +1 -6
- package/dist/command/cli/cli-parse-serve.d.ts +24 -0
- package/dist/command/cli/cli-parse-serve.js +124 -0
- package/dist/command/cli/cli-parse.d.ts +12 -11
- package/dist/command/cli/cli-parse.js +100 -123
- package/dist/command/cli/cli-usage.d.ts +0 -11
- package/dist/command/cli/cli-usage.js +43 -16
- package/dist/command/cli/cli-verify.d.ts +1 -1
- package/dist/command/cli/cli-verify.js +29 -14
- package/dist/command/cli/cloud-cli.js +27 -9
- package/dist/command/cli/cloud-kit.js +3 -3
- package/dist/command/cli/cloud-link.d.ts +1 -20
- package/dist/command/cli/cloud-link.js +2 -0
- package/dist/command/cli/cloud-login.d.ts +6 -2
- package/dist/command/cli/cloud-login.js +13 -41
- package/dist/command/cli/config/checkout.d.ts +10 -0
- package/dist/command/cli/config/checkout.js +78 -0
- package/dist/command/cli/doctor/doctor-agent-link.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-agent-link.js +28 -0
- package/dist/command/cli/doctor/doctor-coverage-line.js +1 -7
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/drive/drive-attach.d.ts +5 -0
- package/dist/command/cli/drive/drive-attach.js +21 -1
- package/dist/command/cli/hook/codex-transcript.d.ts +20 -0
- package/dist/command/cli/hook/codex-transcript.js +224 -0
- package/dist/command/cli/hook/episode-structure.d.ts +24 -0
- package/dist/command/cli/hook/episode-structure.js +138 -0
- package/dist/command/cli/hook/hook-command.d.ts +9 -0
- package/dist/command/cli/hook/hook-command.js +211 -0
- package/dist/command/cli/hook/hook-fast.d.ts +31 -0
- package/dist/command/cli/hook/hook-fast.js +136 -0
- package/dist/command/cli/hook/prompt-hook.d.ts +30 -0
- package/dist/command/cli/hook/prompt-hook.js +199 -0
- package/dist/command/cli/hook/tool-hook.d.ts +21 -0
- package/dist/command/cli/hook/tool-hook.js +259 -0
- package/dist/command/cli/launch/cli-launch.d.ts +2 -0
- package/dist/command/cli/launch/cli-launch.js +22 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +28 -2
- package/dist/command/cli/mcp-command.d.ts +9 -0
- package/dist/command/cli/mcp-command.js +127 -41
- package/dist/command/cli/open-navigate.d.ts +43 -0
- package/dist/command/cli/open-navigate.js +85 -0
- package/dist/command/cli/ports/port-holder.d.ts +16 -4
- package/dist/command/cli/ports/port-holder.js +58 -12
- package/dist/command/cli/ports/resolve/cli-port.js +18 -2
- package/dist/command/cli/project-name.d.ts +1 -7
- package/dist/command/cli/project-name.js +1 -0
- package/dist/command/cli/report-command.d.ts +8 -2
- package/dist/command/cli/report-command.js +95 -18
- package/dist/command/cli/status-fields.d.ts +9 -2
- package/dist/command/cli/status-fields.js +148 -4
- package/dist/command/cli/try-command.d.ts +3 -4
- package/dist/command/cli/try-command.js +6 -11
- package/dist/command/cli/verify-results.d.ts +4 -1
- package/dist/command/cli/verify-results.js +3 -1
- package/dist/command/cli.d.ts +1 -7
- package/dist/command/cli.js +79 -160
- package/dist/command/daemon/daemon-resilience.d.ts +6 -1
- package/dist/command/daemon/daemon-resilience.js +31 -18
- package/dist/command/daemon/daemon.d.ts +11 -0
- package/dist/command/daemon/daemon.js +70 -2
- package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +2 -0
- package/dist/command/daemon/lifetime/daemon-usefulness.js +13 -2
- package/dist/command/daemon/wake-decision.js +4 -2
- package/dist/command/daemon/win32-escape.d.ts +32 -0
- package/dist/command/daemon/win32-escape.js +119 -0
- package/dist/command/setup/agent-configs.d.ts +9 -0
- package/dist/command/setup/agent-configs.js +9 -0
- package/dist/command/setup/auto-approve.d.ts +10 -3
- package/dist/command/setup/auto-approve.js +51 -3
- package/dist/command/setup/bringup/dev-server-files.d.ts +1 -9
- package/dist/command/setup/bringup/dev-server-wait.js +5 -1
- package/dist/command/setup/bringup/ensure-daemon.js +4 -7
- package/dist/command/setup/init/init-host.js +6 -0
- package/dist/command/setup/init/init-port.js +5 -4
- package/dist/command/setup/init/init-runtime.js +7 -1
- package/dist/command/setup/node-effects.d.ts +8 -0
- package/dist/command/setup/node-effects.js +36 -4
- package/dist/command/setup/probe/dev-port.d.ts +26 -0
- package/dist/command/setup/probe/dev-port.js +117 -0
- package/dist/command/setup/probe/page-probe.d.ts +1 -1
- package/dist/command/setup/probe/page-probe.js +11 -3
- package/dist/command/setup/remaining-steps.d.ts +0 -7
- package/dist/command/setup/remaining-steps.js +3 -2
- package/dist/command/setup/run-setup.d.ts +11 -0
- package/dist/command/setup/run-setup.js +33 -5
- package/dist/command/setup/setup-command.d.ts +2 -0
- package/dist/command/setup/setup-command.js +22 -4
- package/dist/command/update/updater.d.ts +1 -6
- package/dist/command/update/updater.js +4 -5
- package/dist/command/update/version-seen.d.ts +24 -0
- package/dist/command/update/version-seen.js +65 -0
- package/dist/command/version/peer-announce.d.ts +9 -1
- package/dist/command/version/peer-announce.js +25 -2
- package/dist/command/version/sdk-fix.d.ts +5 -6
- package/dist/command/version/sdk-fix.js +17 -32
- package/dist/command/version/version-skew.d.ts +16 -1
- package/dist/command/version/version-skew.js +84 -10
- package/dist/features/crawl/crawl-tools.js +2 -2
- package/dist/features/crawl/crawl.js +2 -0
- package/dist/features/exhaust/ledger.d.ts +15 -96
- package/dist/features/exhaust/ledger.js +3 -3
- package/dist/features/harness/drive-runs.d.ts +28 -4
- package/dist/features/harness/drive-runs.js +99 -7
- package/dist/features/harness/drivers.d.ts +1 -1
- package/dist/features/harness/drivers.js +1 -1
- package/dist/features/harness/harness.d.ts +5 -0
- package/dist/features/harness/harness.js +24 -12
- package/dist/features/harness/platform/platform-drives.d.ts +7 -1
- package/dist/features/harness/platform/platform-drives.js +12 -7
- package/dist/features/harness/platform/refusal.d.ts +1 -1
- package/dist/features/harness/platform/refusal.js +4 -4
- package/dist/features/harness/platform/remote-drive.js +70 -28
- package/dist/features/harness/platform/server-driver.js +22 -1
- package/dist/features/harness/platform-config.d.ts +1 -10
- package/dist/features/harness/platform-config.js +0 -20
- package/dist/features/license/license.d.ts +2 -14
- package/dist/features/visual/step-shots.d.ts +33 -1
- package/dist/features/visual/step-shots.js +155 -1
- package/dist/features/visual/visual-tools.js +12 -4
- package/dist/hooks/coding-agents.d.ts +6 -0
- package/dist/hooks/coding-agents.js +24 -3
- package/dist/hooks/episode-stamp.d.ts +32 -0
- package/dist/hooks/episode-stamp.js +61 -0
- package/dist/hooks/hook-commands.js +19 -9
- package/dist/index.d.ts +5 -1
- package/dist/index.js +66 -24
- package/dist/judgement/app-graph/journal-steps.d.ts +8 -0
- package/dist/judgement/app-graph/journal-steps.js +92 -0
- package/dist/judgement/capsule/capsule-store.d.ts +2 -39
- package/dist/judgement/domain/domain-model.js +1 -10
- package/dist/judgement/domain/domain-tools.js +4 -2
- package/dist/judgement/runs/artifact/run-store.d.ts +28 -8
- package/dist/judgement/runs/artifact/run-store.js +153 -28
- package/dist/judgement/runs/default-run-id.d.ts +3 -1
- package/dist/judgement/runs/default-run-id.js +15 -3
- package/dist/judgement/runs/drive-run.d.ts +3 -3
- package/dist/judgement/runs/drive-run.js +11 -7
- package/dist/judgement/runs/run-ledger.d.ts +21 -0
- package/dist/judgement/runs/run-ledger.js +143 -0
- package/dist/judgement/runs/runner-port.d.ts +1 -1
- package/dist/judgement/runs/runner-port.js +8 -2
- package/dist/judgement/runs/verification-sync.d.ts +4 -3
- package/dist/judgement/runs/verification-sync.js +27 -5
- package/dist/language/flows/affected-tools.js +2 -1
- package/dist/language/flows/annotate-notes/annotate.js +8 -3
- package/dist/language/flows/annotate-tools.js +1 -1
- package/dist/language/flows/change/affected.d.ts +8 -0
- package/dist/language/flows/change/affected.js +20 -7
- package/dist/language/flows/decision.js +19 -4
- package/dist/language/flows/fields/flow-secret-field.d.ts +9 -0
- package/dist/language/flows/fields/flow-secret-field.js +67 -5
- package/dist/language/flows/flow-anchor.d.ts +5 -0
- package/dist/language/flows/flow-anchor.js +4 -0
- package/dist/language/flows/flow-cross-step.d.ts +7 -1
- package/dist/language/flows/flow-cross-step.js +9 -0
- package/dist/language/flows/flow-expect-grammar.js +15 -10
- package/dist/language/flows/flow-intent.js +6 -1
- package/dist/language/flows/flow-journey.d.ts +8 -0
- package/dist/language/flows/flow-journey.js +13 -0
- package/dist/language/flows/flow-lease.d.ts +27 -0
- package/dist/language/flows/flow-lease.js +59 -0
- package/dist/language/flows/flow-replay-run.d.ts +1 -87
- package/dist/language/flows/flow-replay-run.js +111 -310
- package/dist/language/flows/flow-replay.js +21 -19
- package/dist/language/flows/flow-replayable.d.ts +38 -0
- package/dist/language/flows/flow-replayable.js +179 -0
- package/dist/language/flows/flow-start-path.d.ts +108 -0
- package/dist/language/flows/flow-start-path.js +313 -0
- package/dist/language/flows/flow-step-runners.d.ts +23 -2
- package/dist/language/flows/flow-step-runners.js +191 -32
- package/dist/language/flows/flow-store-for-session.d.ts +8 -1
- package/dist/language/flows/flow-store-for-session.js +6 -0
- package/dist/language/flows/flow-tools.d.ts +2 -23
- package/dist/language/flows/flow-tools.js +63 -113
- package/dist/language/flows/flows.d.ts +4 -6
- package/dist/language/flows/flows.js +90 -15
- package/dist/language/flows/heal-run.d.ts +10 -4
- package/dist/language/flows/heal-run.js +26 -9
- package/dist/language/flows/recording/flake.d.ts +3 -27
- package/dist/language/flows/recording/flake.js +1 -1
- package/dist/language/flows/recording/recording-backtrack.js +1 -4
- package/dist/language/flows/recording/tape/recordings.d.ts +11 -1
- package/dist/language/flows/recording/tape/recordings.js +9 -3
- package/dist/language/flows/replay.d.ts +25 -5
- package/dist/language/flows/replay.js +83 -16
- package/dist/language/flows/stores/assertion-tiers-store.js +1 -1
- package/dist/language/flows/suite/coverage.d.ts +9 -0
- package/dist/language/flows/suite/coverage.js +12 -1
- package/dist/language/flows/suite-selection.d.ts +25 -0
- package/dist/language/flows/suite-selection.js +41 -9
- package/dist/machine/git-tracks.d.ts +6 -0
- package/dist/machine/git-tracks.js +36 -0
- package/dist/memory/cloud/background-sync.d.ts +25 -0
- package/dist/memory/cloud/background-sync.js +79 -0
- package/dist/memory/cloud/cloud-config.d.ts +12 -1
- package/dist/memory/cloud/cloud-config.js +39 -13
- package/dist/memory/cloud/cloud-sync.d.ts +3 -24
- package/dist/memory/cloud/cloud-sync.js +5 -2
- package/dist/memory/cloud/harness-config.d.ts +0 -7
- package/dist/memory/cloud/memory-scope.d.ts +19 -0
- package/dist/memory/cloud/memory-scope.js +48 -0
- package/dist/memory/cloud/sync-answer.d.ts +43 -0
- package/dist/memory/cloud/sync-answer.js +42 -0
- package/dist/memory/cloud/sync-cycle.d.ts +40 -10
- package/dist/memory/cloud/sync-cycle.js +97 -144
- package/dist/memory/cloud/sync-daemon.d.ts +50 -3
- package/dist/memory/cloud/sync-daemon.js +473 -83
- package/dist/memory/cloud/sync-describe.d.ts +28 -0
- package/dist/memory/cloud/sync-describe.js +92 -0
- package/dist/memory/cloud/sync-disk.d.ts +9 -1
- package/dist/memory/cloud/sync-disk.js +137 -33
- package/dist/memory/cloud/sync-flow-versions.d.ts +26 -0
- package/dist/memory/cloud/sync-flow-versions.js +42 -0
- package/dist/memory/impact/impact-store.js +32 -15
- package/dist/memory/intent/intent-shard.d.ts +41 -266
- package/dist/memory/intent/intent-tools.js +20 -61
- package/dist/memory/intent/record-request.d.ts +27 -0
- package/dist/memory/intent/record-request.js +88 -0
- package/dist/memory/intent/settled-words.js +9 -1
- package/dist/memory/journal/drive-flow.d.ts +3 -0
- package/dist/memory/journal/drive-flow.js +11 -41
- package/dist/memory/journal/envelope-store.js +1 -1
- package/dist/memory/journal/envelope.d.ts +6 -154
- package/dist/memory/journal/episode-log.d.ts +107 -0
- package/dist/memory/journal/episode-log.js +302 -0
- package/dist/memory/journal/journal-recorder.d.ts +7 -0
- package/dist/memory/journal/journal-recorder.js +22 -9
- package/dist/memory/journal/on-disk/ambient-file.d.ts +1 -7
- package/dist/memory/journal/on-disk/ambient-file.js +1 -1
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +24 -3
- package/dist/memory/journal/on-disk/workspace-gitignore.js +38 -7
- package/dist/memory/journal/on-disk/workspace-tiers.js +7 -0
- package/dist/memory/journal/self-observation.js +3 -1
- package/dist/memory/journal/session-end.js +23 -9
- package/dist/memory/journal/session-journal.js +39 -20
- package/dist/memory/project/artifact-root-resolver.d.ts +14 -1
- package/dist/memory/project/artifact-root-resolver.js +68 -23
- package/dist/memory/project/artifact-root.d.ts +17 -3
- package/dist/memory/project/artifact-root.js +47 -9
- package/dist/memory/project/fs/fs-port.d.ts +5 -0
- package/dist/memory/project/fs/fs-port.js +13 -0
- package/dist/memory/project/learned-routes.d.ts +3 -4
- package/dist/memory/project/learned-routes.js +3 -11
- package/dist/memory/project/link-directory.js +5 -2
- package/dist/memory/project/root-claims.d.ts +3 -1
- package/dist/memory/project/root-claims.js +41 -7
- package/dist/memory/project/session-root.d.ts +1 -1
- package/dist/memory/project/session-root.js +1 -15
- package/dist/memory/project/sync-status.js +5 -6
- package/dist/memory/recall/memory-tools.js +2 -2
- package/dist/memory/recall/platform-memory.d.ts +3 -1
- package/dist/memory/recall/platform-memory.js +9 -1
- package/dist/memory/recall/project-memory.js +15 -19
- package/dist/portal/bridge/auth-failure-reason.d.ts +13 -0
- package/dist/portal/bridge/auth-failure-reason.js +19 -0
- package/dist/portal/bridge/bridge.d.ts +19 -4
- package/dist/portal/bridge/bridge.js +150 -177
- package/dist/portal/bridge/harness-request.d.ts +15 -0
- package/dist/portal/bridge/harness-request.js +19 -0
- package/dist/portal/bridge/origin/origin-follow.d.ts +46 -0
- package/dist/portal/bridge/origin/origin-follow.js +110 -0
- package/dist/portal/bridge/pairing-token.d.ts +6 -0
- package/dist/portal/bridge/pairing-token.js +47 -16
- package/dist/portal/bridge/protocol-skew.d.ts +27 -2
- package/dist/portal/bridge/protocol-skew.js +66 -2
- package/dist/portal/bridge/session-nonces.d.ts +46 -0
- package/dist/portal/bridge/session-nonces.js +72 -0
- package/dist/portal/input/viewport-tools.d.ts +31 -0
- package/dist/portal/input/viewport-tools.js +106 -15
- package/dist/portal/pool/bounded-close.d.ts +23 -0
- package/dist/portal/pool/bounded-close.js +38 -0
- package/dist/portal/pool/browser-pool.d.ts +44 -39
- package/dist/portal/pool/browser-pool.js +247 -95
- package/dist/portal/pool/context-permissions.d.ts +9 -1
- package/dist/portal/pool/context-permissions.js +26 -0
- package/dist/portal/pool/lease-emulation.d.ts +37 -0
- package/dist/portal/pool/lease-emulation.js +29 -0
- package/dist/portal/pool/playwright-launcher.d.ts +6 -0
- package/dist/portal/pool/playwright-launcher.js +74 -7
- package/dist/portal/pool/pool-contract.d.ts +23 -1
- package/dist/portal/pool/pool-lease.d.ts +128 -0
- package/dist/portal/pool/pool-lease.js +49 -0
- package/dist/portal/pool/zero-install.d.ts +8 -1
- package/dist/portal/pool/zero-install.js +24 -2
- package/dist/portal/production/production-activation.d.ts +136 -0
- package/dist/portal/production/production-activation.js +173 -0
- package/dist/portal/production/production-drive.d.ts +52 -0
- package/dist/portal/production/production-drive.js +301 -0
- package/dist/portal/production/production-hello.d.ts +23 -0
- package/dist/portal/production/production-hello.js +23 -0
- package/dist/portal/realm/web-realm.js +42 -15
- package/dist/portal/session/control-envelope.d.ts +1 -1
- package/dist/portal/session/dev-server/served-document.d.ts +0 -5
- package/dist/portal/session/dev-server/served-document.js +6 -8
- package/dist/portal/session/facts/handshake-facts.d.ts +8 -1
- package/dist/portal/session/first-run-wiring.d.ts +3 -36
- package/dist/portal/session/first-run-wiring.js +10 -103
- package/dist/portal/session/live-control-tools.js +61 -19
- package/dist/portal/session/no-session-next-action.d.ts +11 -0
- package/dist/portal/session/no-session-next-action.js +40 -15
- package/dist/portal/session/no-session-watch.d.ts +7 -0
- package/dist/portal/session/no-session-watch.js +67 -23
- package/dist/portal/session/output-budget.d.ts +10 -0
- package/dist/portal/session/output-budget.js +16 -0
- package/dist/portal/session/presence/session-lease.d.ts +2 -0
- package/dist/portal/session/presence/session-lease.js +8 -0
- package/dist/portal/session/recorded-gaps.d.ts +15 -11
- package/dist/portal/session/recorded-gaps.js +39 -18
- package/dist/portal/session/serving-directory.d.ts +24 -2
- package/dist/portal/session/serving-directory.js +93 -5
- package/dist/portal/session/session-info.d.ts +26 -0
- package/dist/portal/session/session-info.js +20 -0
- package/dist/portal/session/session-manager.d.ts +35 -3
- package/dist/portal/session/session-manager.js +69 -14
- package/dist/portal/session/session-successor.js +1 -9
- package/dist/portal/session/session-tools.js +22 -4
- package/dist/portal/session/session.d.ts +10 -10
- package/dist/portal/session/session.js +37 -29
- package/dist/portal/session/zero-install-first-call.d.ts +7 -2
- package/dist/portal/session/zero-install-first-call.js +43 -20
- package/dist/start-wiring.d.ts +2 -2
- package/dist/start-wiring.js +17 -3
- package/dist/status-payload.d.ts +11 -1
- package/dist/status-payload.js +7 -1
- package/dist/surface/agent-presence-grace.d.ts +11 -0
- package/dist/surface/agent-presence-grace.js +34 -0
- package/dist/surface/http-server.d.ts +12 -1
- package/dist/surface/http-server.js +64 -15
- package/dist/surface/mcp/faults/mcp-outage.d.ts +12 -2
- package/dist/surface/mcp/faults/mcp-outage.js +19 -1
- package/dist/surface/mcp/mcp-proxy.d.ts +17 -74
- package/dist/surface/mcp/mcp-proxy.js +194 -267
- package/dist/surface/mcp/mcp.d.ts +20 -3
- package/dist/surface/mcp/mcp.js +88 -25
- package/dist/surface/mcp/proxy/drive-calls.d.ts +44 -0
- package/dist/surface/mcp/proxy/drive-calls.js +114 -5
- package/dist/surface/mcp/proxy/proxy-boot.d.ts +11 -0
- package/dist/surface/mcp/proxy/proxy-boot.js +1 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +6 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +14 -1
- package/dist/surface/mcp/proxy/proxy-handshake.d.ts +34 -2
- package/dist/surface/mcp/proxy/proxy-handshake.js +76 -8
- package/dist/surface/mcp/server-instructions.js +35 -16
- package/dist/surface/mcp/shared-params.d.ts +8 -0
- package/dist/surface/mcp/shared-params.js +15 -1
- package/dist/surface/mcp/sse-frame-parser.d.ts +11 -0
- package/dist/surface/mcp/sse-frame-parser.js +18 -0
- package/dist/surface/mcp/surface-vocabulary.d.ts +2 -0
- package/dist/surface/mcp/surface-vocabulary.js +13 -0
- package/dist/surface/mcp/tool-catalog-cache.d.ts +2 -0
- package/dist/surface/mcp/tool-catalog-cache.js +14 -4
- package/dist/surface/tools/account-tool.d.ts +4 -3
- package/dist/surface/tools/account-tool.js +20 -31
- package/dist/surface/tools/act/act-danger.d.ts +2 -0
- package/dist/surface/tools/act/act-danger.js +41 -14
- package/dist/surface/tools/act/act-observation.d.ts +11 -0
- package/dist/surface/tools/act/act-observation.js +33 -0
- package/dist/surface/tools/act/act-preflight.js +10 -3
- package/dist/surface/tools/act/act-sequence-retry.js +2 -1
- package/dist/surface/tools/act/act-target.js +19 -5
- package/dist/surface/tools/act/durable.js +1 -1
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +9 -7
- package/dist/surface/tools/act/navigation/navigate-arrival.js +11 -14
- package/dist/surface/tools/act/unsettled-find.d.ts +36 -0
- package/dist/surface/tools/act/unsettled-find.js +71 -0
- package/dist/surface/tools/act-sequence-tool.js +53 -24
- package/dist/surface/tools/act-tools.d.ts +2 -3
- package/dist/surface/tools/act-tools.js +49 -47
- package/dist/surface/tools/agent/compact-envelope.d.ts +2 -2
- package/dist/surface/tools/agent/for-the-agent.js +41 -3
- package/dist/surface/tools/assert/assert-grade.js +8 -3
- package/dist/surface/tools/assert/assert-verdict.js +4 -2
- package/dist/surface/tools/browser-tools.js +7 -4
- package/dist/surface/tools/chat-drives.js +50 -25
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.d.ts +8 -0
- package/dist/surface/tools/dynamic-tools.js +89 -34
- package/dist/surface/tools/empty-session-list.d.ts +18 -25
- package/dist/surface/tools/empty-session-list.js +67 -38
- package/dist/surface/tools/error-recovery.d.ts +17 -7
- package/dist/surface/tools/error-recovery.js +53 -20
- package/dist/surface/tools/explore-tools.d.ts +10 -0
- package/dist/surface/tools/explore-tools.js +72 -19
- package/dist/surface/tools/feedback-tools.js +23 -2
- package/dist/surface/tools/gap-novelty.d.ts +6 -0
- package/dist/surface/tools/gap-novelty.js +9 -0
- package/dist/surface/tools/gaps/capability-absences.js +4 -3
- package/dist/surface/tools/harness-explore.d.ts +1 -1
- package/dist/surface/tools/harness-explore.js +6 -4
- package/dist/surface/tools/harness-gate.d.ts +43 -21
- package/dist/surface/tools/harness-gate.js +55 -81
- package/dist/surface/tools/harness-script.d.ts +2 -0
- package/dist/surface/tools/harness-script.js +11 -6
- package/dist/surface/tools/harness-toolset.js +3 -4
- package/dist/surface/tools/invoke-tool.d.ts +0 -8
- package/dist/surface/tools/invoke-tool.js +164 -178
- package/dist/surface/tools/lease-hint.d.ts +9 -0
- package/dist/surface/tools/lease-hint.js +22 -3
- package/dist/surface/tools/lease-permissions.d.ts +1 -1
- package/dist/surface/tools/lease-readiness.d.ts +55 -0
- package/dist/surface/tools/lease-readiness.js +150 -2
- package/dist/surface/tools/lease-tools.d.ts +3 -75
- package/dist/surface/tools/lease-tools.js +124 -188
- package/dist/surface/tools/map-tools.d.ts +20 -0
- package/dist/surface/tools/map-tools.js +145 -0
- package/dist/surface/tools/merge-tools.js +11 -7
- package/dist/surface/tools/next-step.js +6 -3
- package/dist/surface/tools/observe-tools.js +46 -35
- package/dist/surface/tools/platform-moment.d.ts +3 -12
- package/dist/surface/tools/platform-moment.js +12 -24
- package/dist/surface/tools/read/complete-snapshot.d.ts +7 -1
- package/dist/surface/tools/read/complete-snapshot.js +13 -2
- package/dist/surface/tools/read/query-paginate.js +6 -0
- package/dist/surface/tools/read/query-shape.d.ts +9 -0
- package/dist/surface/tools/read/query-shape.js +15 -0
- package/dist/surface/tools/read/snapshot-unmounted-note.d.ts +1 -0
- package/dist/surface/tools/read/snapshot-unmounted-note.js +73 -0
- package/dist/surface/tools/read-tools.js +5 -5
- package/dist/surface/tools/real-input-attempt.d.ts +11 -0
- package/dist/surface/tools/real-input-attempt.js +206 -38
- package/dist/surface/tools/reconcile-tools.js +21 -8
- package/dist/surface/tools/reticle-result.d.ts +3 -0
- package/dist/surface/tools/reticle-result.js +8 -1
- package/dist/surface/tools/run-report.d.ts +22 -0
- package/dist/surface/tools/run-report.js +141 -0
- package/dist/surface/tools/session-ports.d.ts +57 -0
- package/dist/surface/tools/session-ports.js +102 -0
- package/dist/surface/tools/tool-kit.d.ts +58 -4
- package/dist/surface/tools/tool-kit.js +140 -6
- package/dist/surface/tools/tool-surface.d.ts +2 -0
- package/dist/surface/tools/tool-surface.js +45 -0
- package/dist/surface/tools/tools.js +49 -87
- package/dist/telemetry/automation-hint.d.ts +5 -0
- package/dist/telemetry/automation-hint.js +7 -0
- package/dist/telemetry/cli-telemetry.d.ts +22 -0
- package/dist/telemetry/cli-telemetry.js +80 -11
- package/dist/telemetry/daemon-telemetry.d.ts +3 -1
- package/dist/telemetry/daemon-telemetry.js +9 -2
- package/dist/telemetry/feedback-cli.d.ts +1 -1
- package/dist/telemetry/feedback-cli.js +8 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/feedback.js +51 -4
- package/dist/telemetry/harness-run.d.ts +3 -3
- package/dist/telemetry/harness-run.js +18 -7
- package/dist/telemetry/hud-events.d.ts +1 -1
- package/dist/telemetry/hud-metrics.d.ts +6 -3
- package/dist/telemetry/hud-metrics.js +20 -8
- package/dist/telemetry/install-source.js +1 -1
- package/dist/telemetry/mcp-connection.d.ts +1 -1
- package/dist/telemetry/mcp-connection.js +21 -2
- package/dist/telemetry/onboarding-firsts.d.ts +6 -1
- package/dist/telemetry/onboarding-firsts.js +41 -1
- package/dist/telemetry/proxy-telemetry.d.ts +42 -0
- package/dist/telemetry/proxy-telemetry.js +70 -1
- package/dist/telemetry/session-metrics.d.ts +1 -3
- package/dist/telemetry/session-metrics.js +16 -12
- package/dist/telemetry/telemetry.d.ts +9 -1
- package/dist/telemetry/telemetry.js +16 -1
- package/dist/telemetry/verification-of.d.ts +3 -1
- package/dist/telemetry/verification-of.js +7 -1
- package/dist/wire-journal.js +12 -2
- package/docs/README.md +4 -2
- package/docs/actions.mdx +7 -6
- package/docs/adapters.md +2 -1
- package/docs/agent-cheatsheet.md +19 -5
- package/docs/architecture.md +13 -10
- package/docs/autodrive.md +6 -5
- package/docs/benchmarks.md +2 -2
- package/docs/capabilities.mdx +1 -1
- package/docs/cli/affected.mdx +1 -1
- package/docs/cli/cloud.mdx +6 -6
- package/docs/cli/doctor.mdx +3 -1
- package/docs/cli/feedback.mdx +11 -7
- package/docs/cli/gate.mdx +1 -1
- package/docs/cli/help.mdx +1 -1
- package/docs/cli/init.mdx +20 -16
- package/docs/cli/mcp.mdx +5 -2
- package/docs/cli/open.mdx +5 -2
- package/docs/cli/report.mdx +1 -1
- package/docs/cli/restart.mdx +1 -1
- package/docs/cli/rollback.mdx +4 -4
- package/docs/cli/serve.mdx +7 -5
- package/docs/cli/setup.mdx +1 -1
- package/docs/cli/status.mdx +6 -2
- package/docs/cli/stop.mdx +3 -3
- package/docs/cli/telemetry.mdx +5 -1
- package/docs/cli/try.mdx +8 -8
- package/docs/cli/update.mdx +8 -5
- package/docs/cli/verify.mdx +12 -5
- package/docs/cli/version.mdx +1 -1
- package/docs/cli.mdx +10 -6
- package/docs/debugging.md +1 -1
- package/docs/deploy-checks.md +8 -6
- package/docs/desktop.mdx +14 -18
- package/docs/enterprise.md +6 -6
- package/docs/environment.mdx +5 -3
- package/docs/faq.mdx +9 -6
- package/docs/first-drive.md +19 -9
- package/docs/flows.md +3 -4
- package/docs/for-agents.md +1 -1
- package/docs/frameworks.mdx +34 -11
- package/docs/getting-started.md +46 -29
- package/docs/http-transport.md +2 -2
- package/docs/human-control.md +8 -8
- package/docs/index.mdx +2 -2
- package/docs/install-agentic.mdx +18 -19
- package/docs/install-manual.mdx +8 -4
- package/docs/integration-patterns.md +5 -5
- package/docs/multi-agent-testing.md +7 -5
- package/docs/packages/browser.mdx +34 -2
- package/docs/packages/core.mdx +2 -1
- package/docs/packages/electron.mdx +15 -3
- package/docs/packages/eslint-plugin.mdx +1 -1
- package/docs/packages/next.mdx +17 -5
- package/docs/packages/react.mdx +8 -1
- package/docs/packages/server.mdx +3 -3
- package/docs/packages/tauri.mdx +3 -3
- package/docs/packages/vite-plugin.mdx +1 -1
- package/docs/packages.mdx +19 -6
- package/docs/platform-integration.md +12 -4
- package/docs/predicates.mdx +18 -7
- package/docs/production-mode.md +108 -0
- package/docs/prompt-capture.md +99 -0
- package/docs/quickstart.mdx +14 -12
- package/docs/recipes.mdx +9 -8
- package/docs/required-checks.md +3 -2
- package/docs/skill-file.mdx +4 -3
- package/docs/skill-setup.md +2 -2
- package/docs/skill.md +62 -70
- package/docs/state-files.md +1 -0
- package/docs/state-management.mdx +11 -9
- package/docs/system-map.md +14 -13
- package/docs/telemetry.md +32 -24
- package/docs/testing.md +9 -5
- package/docs/tools/act-and-wait.mdx +17 -13
- package/docs/tools/act-sequence.mdx +40 -66
- package/docs/tools/act.mdx +7 -5
- package/docs/tools/assert.mdx +1 -1
- package/docs/tools/look.mdx +1 -1
- package/docs/tools/navigate.mdx +1 -1
- package/docs/tools/network.mdx +13 -11
- package/docs/tools/overview.mdx +3 -3
- package/docs/tools/query.mdx +3 -3
- package/docs/tools/session-and-feedback.mdx +9 -6
- package/docs/tools/sessions.mdx +7 -0
- package/docs/tools/snapshot.mdx +9 -7
- package/docs/tools/tools-and-run.mdx +11 -10
- package/docs/tools/verify.mdx +1 -1
- package/docs/troubleshooting.mdx +21 -9
- package/docs/usage.md +58 -43
- package/docs/use-cases.mdx +2 -2
- package/docs/vs-chrome-devtools-mcp.mdx +5 -5
- package/docs/vs-playwright-mcp.mdx +5 -4
- package/docs/what-is-recorded.md +12 -7
- package/docs/what-is-reticle.mdx +2 -2
- package/docs/why-reticle.mdx +1 -1
- package/package.json +9 -12
- package/dist/command/dev/stale-issue-guard.d.ts +0 -34
- package/dist/command/dev/stale-issue-guard.js +0 -69
|
@@ -10,9 +10,13 @@
|
|
|
10
10
|
* work is happening" costs nobody anything.
|
|
11
11
|
*
|
|
12
12
|
* ── THE THREE RULES THAT KEEP IT INVISIBLE ────────────────────────────────────────────────────
|
|
13
|
-
* QUIET WHEN IDLE. A cycle
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* QUIET WHEN IDLE. A cycle runs because something under `.reticle` CHANGED: the whole root is
|
|
14
|
+
* watched, and a change schedules one cycle after a short debounce, so a drive that writes forty
|
|
15
|
+
* files costs one cycle. Nothing changed, nothing sent. A slow timer (ten minutes) stays as the
|
|
16
|
+
* backstop — for a filesystem that cannot be watched and the team's decisions to pull down — and a
|
|
17
|
+
* cycle that moved something still earns the fast active rate. A FAILED cycle retries after a
|
|
18
|
+
* minute, doubling per failure up to the backstop, and a success returns it to the idle rhythm.
|
|
19
|
+
* The sync's own bookkeeping writes are ignored, or every cycle would schedule the next.
|
|
16
20
|
*
|
|
17
21
|
* NEVER IN THE WAY. Nothing here is awaited by a tool call. A sync that can slow down or fail a
|
|
18
22
|
* verification would be a sync worth switching off, and the local record is authoritative anyway.
|
|
@@ -21,23 +25,80 @@
|
|
|
21
25
|
* laptop on a train would otherwise fill the log with the same line four hundred times and teach
|
|
22
26
|
* everyone to ignore it.
|
|
23
27
|
*/
|
|
28
|
+
import { ReticleDir } from '@reticlehq/core';
|
|
24
29
|
import { reticleDirPaths } from '../project/dir/reticle-dir.js';
|
|
25
30
|
import { syncRequest } from './cloud-sync.js';
|
|
26
31
|
import { log } from '../../log.js';
|
|
27
32
|
import { homedir } from 'node:os';
|
|
28
33
|
import { emitSyncHook } from '../../hooks/hook-emit.js';
|
|
29
|
-
import { MemoryReadHeader, MemoryReadPurpose, memoryReadUrl, scopedMemoryEntries, } from './memory-scope.js';
|
|
34
|
+
import { MemoryReadHeader, MemoryReadPurpose, MemoryScopeField, MEMORY_PAGE_LIMIT, memoryReadUrl, readMemoryPages, scopedMemoryEntries, } from './memory-scope.js';
|
|
30
35
|
import { IntentShardStore } from '../intent/intent-shard-store.js';
|
|
31
36
|
import { applySettledWords, keptWordsInRequest, settledWordsIn, } from '../intent/settled-words.js';
|
|
32
37
|
import { createNodeFileSystem } from '../project/fs/fs-port.js';
|
|
33
38
|
import { flushPendingWrites } from '../../hooks/hook-bus.js';
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
39
|
+
import { runSyncCycle } from './sync-cycle.js';
|
|
40
|
+
import { describeSync } from './sync-describe.js';
|
|
41
|
+
import { diskSink, diskSource, policyScreenshots, readCloudState } from './sync-disk.js';
|
|
42
|
+
import { resolveProjectCloud } from './cloud-config.js';
|
|
37
43
|
import { watch } from 'node:fs';
|
|
44
|
+
import { BackgroundSyncState, backgroundSyncGate, } from './background-sync.js';
|
|
38
45
|
import { unmatchedRoots, unsyncedRoots } from './unsynced-roots.js';
|
|
39
|
-
/**
|
|
40
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The backstop: how often the daemon cycles when nothing under `.reticle` has changed. Long, because
|
|
48
|
+
* a change wakes the sync on its own; this exists for what a watch cannot see (an unwatchable
|
|
49
|
+
* filesystem, decisions made on the dashboard). A failed cycle retries sooner: FAILURE_RETRY_MS.
|
|
50
|
+
*/
|
|
51
|
+
const DAEMON_SYNC_INTERVAL_MS = 600_000;
|
|
52
|
+
/**
|
|
53
|
+
* How long a change under `.reticle` waits before cycling. Coalesces the burst a drive writes, and
|
|
54
|
+
* is never extended by a later write, so a stream of writes cannot postpone the sync forever.
|
|
55
|
+
*/
|
|
56
|
+
const CHANGE_DEBOUNCE_MS = 2_000;
|
|
57
|
+
/**
|
|
58
|
+
* What the sync itself writes into a root, plus `sessions/` (the per-event journal, appended on
|
|
59
|
+
* every browser event and never sent) and `episodes/` (every tool call, likewise never sent). A
|
|
60
|
+
* change here must not wake the sync: the first would make every cycle schedule the next, the others
|
|
61
|
+
* would turn an open tab or a working agent into a cycle every two seconds.
|
|
62
|
+
* The atomic writes' `<file>.<pid>.tmp` siblings are matched by prefix.
|
|
63
|
+
*/
|
|
64
|
+
const NOT_A_CHANGE = [ReticleDir.CLOUD_STATE_FILE, ReticleDir.ISSUES_FILE];
|
|
65
|
+
/**
|
|
66
|
+
* The longest the team's memory decisions go unread while a drive keeps the sync busy. They must come
|
|
67
|
+
* down before changed INTENT goes up (or a word a maintainer set aside is sent back as a new
|
|
68
|
+
* disagreement), and nothing else waits on them: paging the whole memory before every run cost a
|
|
69
|
+
* real project nine round trips per cycle, the larger part of a run's time to the dashboard.
|
|
70
|
+
*/
|
|
71
|
+
const SETTLED_WORDS_MAX_AGE_MS = 60_000;
|
|
72
|
+
/**
|
|
73
|
+
* How often the OTHER linked repos are walked while this one keeps changing. They are not watched, and
|
|
74
|
+
* each is a full cycle of round trips: walking them on every change here made a run landing in this
|
|
75
|
+
* repo wait behind all of them. The backstop timer and a requested sync always walk them.
|
|
76
|
+
*/
|
|
77
|
+
const OTHER_ROOTS_EVERY_MS = 60_000;
|
|
78
|
+
/**
|
|
79
|
+
* How often an idle machine asks whether the team's memory moved. Conditional (If-None-Match on the
|
|
80
|
+
* first page), so an unchanged minute is one bodiless 304, and a dashboard decision lands within it
|
|
81
|
+
* rather than on the ten-minute backstop.
|
|
82
|
+
*/
|
|
83
|
+
const MEMORY_POLL_MS = 50_000;
|
|
84
|
+
/**
|
|
85
|
+
* How often the poll looks at the clock. A read is due once MEMORY_POLL_MS have passed since the last
|
|
86
|
+
* one (any read, a cycle's too), so a decision waits at most the two together: one minute. Measured
|
|
87
|
+
* with a plain one-minute timer: 74 s and 96 s, because a cycle's read just before it pushed the
|
|
88
|
+
* next ask a whole minute on.
|
|
89
|
+
*/
|
|
90
|
+
const MEMORY_POLL_TICK_MS = 10_000;
|
|
91
|
+
const IF_NONE_MATCH = 'if-none-match';
|
|
92
|
+
const isIntent = (path) => {
|
|
93
|
+
const [head = ''] = path.split(/[\\/]/);
|
|
94
|
+
return head === ReticleDir.INTENT_SUBDIR || head.startsWith(ReticleDir.INTENT_FILE);
|
|
95
|
+
};
|
|
96
|
+
const isOwnWrite = (path) => {
|
|
97
|
+
const [head = ''] = path.split(/[\\/]/);
|
|
98
|
+
return (head === ReticleDir.SESSIONS_SUBDIR ||
|
|
99
|
+
head === ReticleDir.EPISODES_SUBDIR ||
|
|
100
|
+
NOT_A_CHANGE.some((file) => head === file || head.startsWith(`${file}.`)));
|
|
101
|
+
};
|
|
41
102
|
/**
|
|
42
103
|
* The cadence while something is actually happening.
|
|
43
104
|
*
|
|
@@ -53,6 +114,11 @@ const DAEMON_SYNC_INTERVAL_MS = 60_000;
|
|
|
53
114
|
* loop, and verification working with the network down is a promise this product makes.
|
|
54
115
|
*/
|
|
55
116
|
const DAEMON_SYNC_ACTIVE_INTERVAL_MS = 5_000;
|
|
117
|
+
/**
|
|
118
|
+
* The first retry after a failed cycle, doubling with each failure after it up to the backstop. A
|
|
119
|
+
* failed upload is owed work, so it does not wait ten minutes for the idle timer.
|
|
120
|
+
*/
|
|
121
|
+
const FAILURE_RETRY_MS = 60_000;
|
|
56
122
|
/** Given to the first cycle so a freshly-started daemon does not race the session that woke it. */
|
|
57
123
|
const FIRST_CYCLE_DELAY_MS = 5_000;
|
|
58
124
|
/**
|
|
@@ -65,14 +131,16 @@ const FIRST_CYCLE_DELAY_MS = 5_000;
|
|
|
65
131
|
*/
|
|
66
132
|
const NUDGE_DELAY_MS = 1_500;
|
|
67
133
|
/**
|
|
68
|
-
*
|
|
134
|
+
* The whole time a flush may take: the wait for a cycle in flight, and its own cycle.
|
|
69
135
|
*
|
|
70
|
-
* Shutdown budget, not a sync budget
|
|
71
|
-
*
|
|
72
|
-
*
|
|
136
|
+
* Shutdown budget, not a sync budget. `reticle stop` sends SIGKILL 5 s after SIGTERM, and the daemon
|
|
137
|
+
* sends its stop event (up to 2 s) before it flushes. A flush past this budget let the kill come
|
|
138
|
+
* first. Runs that are not sent stay on disk, and the next daemon sends them.
|
|
73
139
|
*/
|
|
74
|
-
const
|
|
140
|
+
const FLUSH_BUDGET_MS = 1_500;
|
|
75
141
|
const FLUSH_POLL_MS = 25;
|
|
142
|
+
/** A failed status, push or pull that says the platform refused the credential. */
|
|
143
|
+
const AUTH_REFUSED = /^(?:status|sync|pull) 40[13]\b/;
|
|
76
144
|
/** Whether a cycle landed or collected anything — what earns the fast interval and a log line. */
|
|
77
145
|
const movedAnything = (report) => report.runsSent > 0 ||
|
|
78
146
|
report.flowsSent > 0 ||
|
|
@@ -91,6 +159,13 @@ export function startSyncDaemon(deps) {
|
|
|
91
159
|
const activeIntervalMs = Math.min(DAEMON_SYNC_ACTIVE_INTERVAL_MS, intervalMs);
|
|
92
160
|
const now = deps.now ?? (() => Date.now());
|
|
93
161
|
const request = deps.request ?? defaultRequest;
|
|
162
|
+
/** Per root, the credential the platform refused (401/403). Stopped until the sign-in changes. */
|
|
163
|
+
const refusedUnder = new Map();
|
|
164
|
+
/** Per root, the gate as of this tick. */
|
|
165
|
+
const gates = new Map();
|
|
166
|
+
/** Per root, the state last logged: one line per change, never one per tick. */
|
|
167
|
+
const loggedState = new Map();
|
|
168
|
+
let lastPushAt;
|
|
94
169
|
let timer;
|
|
95
170
|
let stopped = false;
|
|
96
171
|
let running = false;
|
|
@@ -131,8 +206,9 @@ export function startSyncDaemon(deps) {
|
|
|
131
206
|
[...linked.keys(), ...unmatchedRoots(homedir())], (r) => Promise.resolve(true === linked.get(r)), (r) => Promise.resolve(sendsFlows.has(r)));
|
|
132
207
|
for (const entry of unsynced) {
|
|
133
208
|
const held = entry.runs + entry.flows;
|
|
134
|
-
if (entry.linked || announced.get(entry.root) === held)
|
|
209
|
+
if (entry.linked || announced.get(entry.root) === held) {
|
|
135
210
|
continue;
|
|
211
|
+
}
|
|
136
212
|
announced.set(entry.root, held);
|
|
137
213
|
log('reticle_cloud_unsynced_root', {
|
|
138
214
|
root: entry.root,
|
|
@@ -143,15 +219,63 @@ export function startSyncDaemon(deps) {
|
|
|
143
219
|
});
|
|
144
220
|
}
|
|
145
221
|
};
|
|
222
|
+
/**
|
|
223
|
+
* The cloud as this tick may use it: unchanged when the gate lets the root send, else with no
|
|
224
|
+
* config, which every caller below already reads as "send nothing". A credential the platform
|
|
225
|
+
* refused stays refused until the sign-in or the key changes.
|
|
226
|
+
*/
|
|
227
|
+
const admit = (root, cloud) => {
|
|
228
|
+
if (deps.gate === undefined) {
|
|
229
|
+
return cloud;
|
|
230
|
+
}
|
|
231
|
+
let gate = deps.gate(root, cloud);
|
|
232
|
+
if (BackgroundSyncState.ON === gate.state && gate.credential !== undefined) {
|
|
233
|
+
if (refusedUnder.get(root) === gate.credential) {
|
|
234
|
+
gate = {
|
|
235
|
+
state: BackgroundSyncState.REFUSED,
|
|
236
|
+
why: 'the platform refused this sign-in: run `reticle connect` again',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
refusedUnder.delete(root);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
gates.set(root, gate);
|
|
244
|
+
// A root starts as unlinked: `reticle_cloud_unsynced_root` already names one that never linked.
|
|
245
|
+
if ((loggedState.get(root) ?? BackgroundSyncState.UNLINKED) !== gate.state) {
|
|
246
|
+
loggedState.set(root, gate.state);
|
|
247
|
+
log('reticle_background_sync', { root, state: gate.state, why: gate.why });
|
|
248
|
+
}
|
|
249
|
+
return BackgroundSyncState.ON === gate.state ? cloud : { ...cloud, config: null };
|
|
250
|
+
};
|
|
251
|
+
/** Stop sending for this root until its sign-in changes. */
|
|
252
|
+
const refuse = (root) => {
|
|
253
|
+
const credential = gates.get(root)?.credential;
|
|
254
|
+
if (credential !== undefined) {
|
|
255
|
+
refusedUnder.set(root, credential);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
146
258
|
/** One root's bundle. The state, source and sink are already per-root; only the caller was not. */
|
|
147
|
-
const pushRoot = async (root, cloud) => {
|
|
148
|
-
if (null === cloud.config)
|
|
259
|
+
const pushRoot = async (root, cloud, readMemory = true) => {
|
|
260
|
+
if (null === cloud.config) {
|
|
149
261
|
return undefined;
|
|
262
|
+
}
|
|
263
|
+
const report = await sendRoot(root, cloud, readMemory);
|
|
264
|
+
if (report.error !== undefined && AUTH_REFUSED.test(report.error)) {
|
|
265
|
+
refuse(root);
|
|
266
|
+
}
|
|
267
|
+
return report;
|
|
268
|
+
};
|
|
269
|
+
const sendRoot = async (root, cloud, readMemory) => {
|
|
270
|
+
if (null === cloud.config) {
|
|
271
|
+
throw new Error('sendRoot needs a linked cloud');
|
|
272
|
+
}
|
|
150
273
|
const full = diskSource(root);
|
|
151
274
|
// The team's decisions come down too: an author's record kept the words a maintainer set aside,
|
|
152
275
|
// and re-sent them every cycle as a new disagreement.
|
|
153
|
-
if (cloud.policy.memory)
|
|
276
|
+
if (cloud.policy.memory && readMemory) {
|
|
154
277
|
await pullSettledWords(root, cloud.config, cloud.projectId).catch(() => 0);
|
|
278
|
+
}
|
|
155
279
|
return runSyncCycle({
|
|
156
280
|
config: cloud.config,
|
|
157
281
|
...(cloud.link === undefined ? {} : { link: cloud.link }),
|
|
@@ -162,33 +286,97 @@ export function startSyncDaemon(deps) {
|
|
|
162
286
|
flows: () => (cloud.policy.flows ? full.flows() : []),
|
|
163
287
|
capsules: () => (cloud.policy.capsules ? full.capsules() : []),
|
|
164
288
|
derived: (kind) => (cloud.policy.memory ? full.derived(kind) : undefined),
|
|
165
|
-
...(
|
|
166
|
-
? { screenshot: full.screenshot }
|
|
167
|
-
: {}),
|
|
289
|
+
...policyScreenshots(full.screenshot, cloud.policy),
|
|
168
290
|
},
|
|
169
291
|
sink: diskSink(root),
|
|
170
292
|
state: readCloudState(root),
|
|
171
293
|
now,
|
|
294
|
+
...(deps.clientVersion === undefined ? {} : { clientVersion: deps.clientVersion }),
|
|
172
295
|
request,
|
|
173
296
|
});
|
|
174
297
|
};
|
|
298
|
+
/** The tag of this root's first memory page as last read: what a conditional read names. */
|
|
299
|
+
let memoryTag;
|
|
300
|
+
let polling = false;
|
|
301
|
+
let lastCloud;
|
|
175
302
|
/** Bring the dashboard's memory decisions down to this root's records. Returns how many changed. */
|
|
176
|
-
const pullSettledWords = async (root, config, projectId
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
303
|
+
const pullSettledWords = async (root, config, projectId,
|
|
304
|
+
/** A first page already in hand (the poll's 200), so it is not asked for twice. */
|
|
305
|
+
first) => {
|
|
306
|
+
const body = await readMemoryPages(config.url, { projectId }, async (url) => {
|
|
307
|
+
if (first !== undefined && url === first.url) {
|
|
308
|
+
return first.body;
|
|
309
|
+
}
|
|
310
|
+
const res = await request(url, {
|
|
311
|
+
method: 'GET',
|
|
312
|
+
// The sync, not an agent: without this every cycle counted as an agent consulting memory.
|
|
313
|
+
headers: {
|
|
314
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
315
|
+
[MemoryReadHeader.PURPOSE]: MemoryReadPurpose.SYNC,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
if (root === deps.reticleRoot && !url.includes(`${MemoryScopeField.CURSOR}=`)) {
|
|
319
|
+
memoryTag = res.etag;
|
|
320
|
+
}
|
|
321
|
+
return 200 === res.status ? JSON.parse(res.text) : undefined;
|
|
184
322
|
});
|
|
185
|
-
if (
|
|
323
|
+
if (body === undefined) {
|
|
186
324
|
return 0;
|
|
187
|
-
|
|
325
|
+
}
|
|
326
|
+
const entries = scopedMemoryEntries(body, projectId) ?? [];
|
|
188
327
|
const settled = settledWordsIn(entries);
|
|
189
328
|
await keptWordsInRequest(reticleDirPaths(root).request, settled);
|
|
190
329
|
return applySettledWords(new IntentShardStore(createNodeFileSystem(), root, { now }), settled);
|
|
191
330
|
};
|
|
331
|
+
/**
|
|
332
|
+
* Ask whether the team's memory moved and, when it did, bring its decisions down. Skipped while a
|
|
333
|
+
* cycle runs or one read it within the minute.
|
|
334
|
+
* ponytail: the tag covers the first page, which is ordered newest-updated first, so any decision
|
|
335
|
+
* moves it; a fact deleted past the first page waits for the backstop.
|
|
336
|
+
*/
|
|
337
|
+
const pollMemory = async () => {
|
|
338
|
+
if (stopped || running || polling || now() - memoryReadAt < MEMORY_POLL_MS) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
polling = true;
|
|
342
|
+
try {
|
|
343
|
+
// The link the last cycle resolved: the poll costs no link read of its own.
|
|
344
|
+
const cloud = lastCloud;
|
|
345
|
+
if (cloud === undefined || null === cloud.config || !cloud.policy.memory) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const url = memoryReadUrl(cloud.config.url, {
|
|
349
|
+
projectId: cloud.projectId,
|
|
350
|
+
limit: MEMORY_PAGE_LIMIT,
|
|
351
|
+
});
|
|
352
|
+
const res = await request(url, {
|
|
353
|
+
method: 'GET',
|
|
354
|
+
headers: {
|
|
355
|
+
authorization: `Bearer ${cloud.config.apiKey}`,
|
|
356
|
+
[MemoryReadHeader.PURPOSE]: MemoryReadPurpose.SYNC,
|
|
357
|
+
...(memoryTag === undefined ? {} : { [IF_NONE_MATCH]: memoryTag }),
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
// Answered either way, 304 included: the memory is known current as of now.
|
|
361
|
+
if (200 === res.status || 304 === res.status) {
|
|
362
|
+
memoryReadAt = now();
|
|
363
|
+
}
|
|
364
|
+
if (200 !== res.status) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
memoryTag = res.etag;
|
|
368
|
+
await pullSettledWords(deps.reticleRoot, cloud.config, cloud.projectId, {
|
|
369
|
+
url,
|
|
370
|
+
body: JSON.parse(res.text),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// Offline is not news; the next minute asks again.
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
polling = false;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
192
380
|
/**
|
|
193
381
|
* Push every other linked root this machine knows about.
|
|
194
382
|
*
|
|
@@ -199,8 +387,9 @@ export function startSyncDaemon(deps) {
|
|
|
199
387
|
const syncOtherRoots = async () => {
|
|
200
388
|
const roots = deps.otherRoots;
|
|
201
389
|
const cloudFor = deps.cloudFor;
|
|
202
|
-
if (roots === undefined || cloudFor === undefined)
|
|
390
|
+
if (roots === undefined || cloudFor === undefined) {
|
|
203
391
|
return;
|
|
392
|
+
}
|
|
204
393
|
let list = [];
|
|
205
394
|
try {
|
|
206
395
|
list = await roots();
|
|
@@ -211,35 +400,41 @@ export function startSyncDaemon(deps) {
|
|
|
211
400
|
return;
|
|
212
401
|
}
|
|
213
402
|
for (const root of list) {
|
|
214
|
-
if (root === deps.reticleRoot)
|
|
403
|
+
if (root === deps.reticleRoot) {
|
|
215
404
|
continue;
|
|
405
|
+
}
|
|
216
406
|
rootsLinked.set(root, false);
|
|
217
407
|
try {
|
|
218
|
-
const cloud = await cloudFor(root);
|
|
408
|
+
const cloud = admit(root, await cloudFor(root));
|
|
219
409
|
// Linked HERE, by its own link file. An unlinked root resolves to the environment's key,
|
|
220
410
|
// which is the right answer for the root this daemon stands in and somebody else's project
|
|
221
411
|
// for every other one: a daemon started with RETICLE_API_KEY pushed every project this
|
|
222
412
|
// machine had seen into that one, a status and a pull per root on every cycle.
|
|
223
413
|
// Never silently: `refreshUnsynced` names this root if it holds runs.
|
|
224
|
-
if (null === cloud.config || null === cloud.projectId)
|
|
414
|
+
if (null === cloud.config || null === cloud.projectId) {
|
|
225
415
|
continue;
|
|
416
|
+
}
|
|
226
417
|
rootsLinked.set(root, true);
|
|
227
|
-
if (cloud.policy.flows)
|
|
418
|
+
if (cloud.policy.flows) {
|
|
228
419
|
rootsSendFlows.add(root);
|
|
420
|
+
}
|
|
229
421
|
const report = await pushRoot(root, cloud);
|
|
230
|
-
if (report === undefined)
|
|
422
|
+
if (report === undefined) {
|
|
231
423
|
continue;
|
|
424
|
+
}
|
|
232
425
|
if (report.error !== undefined) {
|
|
233
426
|
log('reticle_cloud_sync_failed', { root, error: report.error });
|
|
234
427
|
continue;
|
|
235
428
|
}
|
|
236
|
-
if (!report.ok)
|
|
429
|
+
if (!report.ok) {
|
|
237
430
|
log('reticle_cloud_sync_failed', { root, error: describeSync(report) });
|
|
431
|
+
}
|
|
238
432
|
const moved = movedAnything(report);
|
|
239
433
|
// The root is NAMED here and not in the single-root log below, because with several repos
|
|
240
434
|
// reporting, "synced 3 runs" without a directory is not an answer to "synced from where".
|
|
241
|
-
if (moved)
|
|
435
|
+
if (moved) {
|
|
242
436
|
log('reticle_cloud_synced', { root, summary: describeSync(report) });
|
|
437
|
+
}
|
|
243
438
|
}
|
|
244
439
|
catch (error) {
|
|
245
440
|
log('reticle_cloud_sync_failed', {
|
|
@@ -249,46 +444,84 @@ export function startSyncDaemon(deps) {
|
|
|
249
444
|
}
|
|
250
445
|
}
|
|
251
446
|
};
|
|
252
|
-
|
|
447
|
+
/** When this root last read the team's memory, and whether its intent moved since. */
|
|
448
|
+
let memoryReadAt = Number.NEGATIVE_INFINITY;
|
|
449
|
+
/** When the other linked repos were last walked. */
|
|
450
|
+
let othersAt = Number.NEGATIVE_INFINITY;
|
|
451
|
+
let intentMoved = true;
|
|
452
|
+
/**
|
|
453
|
+
* A change landed that no cycle has delivered yet. Work saved while offline used to wait out the
|
|
454
|
+
* longest backoff after the network returned (each save's failed cycle doubled it): owed work
|
|
455
|
+
* retries every minute, and only an idle machine backs off.
|
|
456
|
+
*/
|
|
457
|
+
let owed = false;
|
|
458
|
+
const memoryDue = (forced) => {
|
|
459
|
+
if (!forced && !intentMoved && now() - memoryReadAt < SETTLED_WORDS_MAX_AGE_MS) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
intentMoved = false;
|
|
463
|
+
memoryReadAt = now();
|
|
464
|
+
return true;
|
|
465
|
+
};
|
|
466
|
+
/** `asked`: a sync somebody requested: read the memory and walk every repo, whatever is due. */
|
|
467
|
+
const cycle = async (asked = false) => {
|
|
253
468
|
// Overlap guard: a slow cycle must not have a second one started on top of it, or two bundles
|
|
254
469
|
// race and the cursor written by the loser silently rewinds the winner's progress.
|
|
255
|
-
if (running)
|
|
470
|
+
if (running) {
|
|
256
471
|
return undefined;
|
|
472
|
+
}
|
|
257
473
|
running = true;
|
|
258
|
-
|
|
259
|
-
|
|
474
|
+
// Owed work this cycle carries; a failed exchange leaves it owed, and a change meanwhile adds to it.
|
|
475
|
+
const carried = owed;
|
|
476
|
+
owed = false;
|
|
477
|
+
// The other repos' entries stand until their next pass, so what they owe is still reported.
|
|
478
|
+
const othersDue = asked || now() - othersAt >= OTHER_ROOTS_EVERY_MS;
|
|
479
|
+
if (othersDue) {
|
|
480
|
+
othersAt = now();
|
|
481
|
+
rootsLinked = new Map();
|
|
482
|
+
rootsSendFlows = new Set();
|
|
483
|
+
}
|
|
484
|
+
const own = deps.reticleRoot;
|
|
485
|
+
rootsSendFlows.delete(own);
|
|
260
486
|
try {
|
|
261
|
-
const cloud = await deps.cloud();
|
|
487
|
+
const cloud = admit(deps.reticleRoot, await deps.cloud());
|
|
488
|
+
lastCloud = cloud;
|
|
262
489
|
const linked = null !== cloud.config;
|
|
263
|
-
const own = deps.reticleRoot;
|
|
264
490
|
rootsLinked.set(own, linked);
|
|
265
|
-
if (linked && cloud.policy.flows)
|
|
491
|
+
if (linked && cloud.policy.flows) {
|
|
266
492
|
rootsSendFlows.add(own);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
493
|
+
}
|
|
494
|
+
// Another project's memory is another project's decisions: a re-link reads it at once.
|
|
495
|
+
const relinked = cloud.projectId !== wasProject;
|
|
496
|
+
const linkChanged = linked !== wasLinked || (linked && relinked);
|
|
497
|
+
wasLinked = linked;
|
|
498
|
+
wasProject = cloud.projectId;
|
|
499
|
+
// With a gate, `admit` already said why once per change; this would say "run connect" to
|
|
500
|
+
// somebody who only signed out or switched sync off.
|
|
501
|
+
if (deps.gate === undefined && linkChanged) {
|
|
502
|
+
if (linked) {
|
|
271
503
|
log('reticle_cloud_linked', {
|
|
272
504
|
projectId: cloud.projectId,
|
|
273
505
|
root: deps.reticleRoot,
|
|
274
506
|
});
|
|
507
|
+
}
|
|
275
508
|
// Names the ROOT, because the answer is almost always "the daemon is not where you think it
|
|
276
509
|
// is" — and a message that omits the directory sends people to check their key instead.
|
|
277
|
-
else
|
|
510
|
+
else {
|
|
278
511
|
log('reticle_cloud_unlinked', {
|
|
279
512
|
root: deps.reticleRoot,
|
|
280
513
|
fix: 'run `reticle connect` in this directory, or start the daemon in the linked one — nothing is being synced from here',
|
|
281
514
|
});
|
|
515
|
+
}
|
|
282
516
|
}
|
|
283
|
-
|
|
284
|
-
// push: one repo whose credential was revoked must not silence the rest of the machine.
|
|
285
|
-
await syncOtherRoots();
|
|
286
|
-
if (null === cloud.config)
|
|
517
|
+
if (null === cloud.config) {
|
|
287
518
|
return undefined;
|
|
288
|
-
|
|
519
|
+
}
|
|
520
|
+
const report = await pushRoot(deps.reticleRoot, cloud, memoryDue(asked || relinked));
|
|
289
521
|
// `pushRoot` returns undefined only for an unlinked root, which the guard above has ruled out.
|
|
290
|
-
if (report === undefined)
|
|
522
|
+
if (report === undefined) {
|
|
291
523
|
return undefined;
|
|
524
|
+
}
|
|
292
525
|
// Said out loud on EVERY cycle, including the ones that moved nothing and the ones that
|
|
293
526
|
// failed. The log below is deliberately quiet when idle, which is right for a log and wrong
|
|
294
527
|
// for a hook: from outside, "nothing changed" and "sync has been broken since Tuesday" are
|
|
@@ -307,21 +540,36 @@ export function startSyncDaemon(deps) {
|
|
|
307
540
|
reportedError = failure;
|
|
308
541
|
log('reticle_cloud_sync_failed', { error: failure });
|
|
309
542
|
}
|
|
310
|
-
if (report.error === undefined) {
|
|
311
|
-
|
|
543
|
+
if (report.error === undefined && report.ok) {
|
|
544
|
+
lastPushAt = now();
|
|
545
|
+
}
|
|
546
|
+
if (report.error !== undefined) {
|
|
547
|
+
owed ||= carried;
|
|
548
|
+
failedCycle();
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
if (failure === undefined) {
|
|
312
552
|
reportedError = undefined;
|
|
553
|
+
}
|
|
313
554
|
// Only when something actually moved. A per-minute "nothing to send" is noise that trains
|
|
314
555
|
// people to stop reading the log.
|
|
315
556
|
const moved = movedAnything(report);
|
|
316
|
-
if (moved && failure === undefined)
|
|
557
|
+
if (moved && failure === undefined) {
|
|
317
558
|
log('reticle_cloud_synced', { summary: describeSync(report) });
|
|
559
|
+
}
|
|
318
560
|
/*
|
|
319
561
|
* A cycle that moved something means a drive is in progress, so the next one comes sooner;
|
|
320
562
|
* one that moved nothing means the machine is idle, so it backs straight off. The rate
|
|
321
563
|
* follows the work without needing to be told about sessions, and an idle laptop settles at
|
|
322
564
|
* the same cost it had before this existed.
|
|
323
565
|
*/
|
|
324
|
-
|
|
566
|
+
if (failure !== undefined && !moved) {
|
|
567
|
+
failedCycle();
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
failures = 0;
|
|
571
|
+
nextDelay = moved ? activeIntervalMs : intervalMs;
|
|
572
|
+
}
|
|
325
573
|
}
|
|
326
574
|
return report;
|
|
327
575
|
}
|
|
@@ -332,66 +580,177 @@ export function startSyncDaemon(deps) {
|
|
|
332
580
|
reportedError = message;
|
|
333
581
|
log('reticle_cloud_sync_failed', { error: message });
|
|
334
582
|
}
|
|
583
|
+
owed ||= carried;
|
|
584
|
+
failedCycle();
|
|
335
585
|
return undefined;
|
|
336
586
|
}
|
|
337
587
|
finally {
|
|
588
|
+
// Every OTHER linked root, after this one: the root that changed is the one somebody is waiting
|
|
589
|
+
// on. Their failures are logged and never touch this root's push, whichever ran first.
|
|
590
|
+
if (othersDue) {
|
|
591
|
+
await syncOtherRoots().catch(() => undefined);
|
|
592
|
+
}
|
|
338
593
|
await refreshUnsynced().catch(() => undefined);
|
|
339
594
|
running = false;
|
|
595
|
+
startWatching();
|
|
340
596
|
}
|
|
341
597
|
};
|
|
342
598
|
/** The delay for the NEXT cycle: fast while the last one moved something, slow once it stops. */
|
|
343
599
|
let nextDelay = intervalMs;
|
|
600
|
+
/** Consecutive failed cycles: the retry waits a minute, then doubles, never past the backstop. */
|
|
601
|
+
let failures = 0;
|
|
602
|
+
function failedCycle() {
|
|
603
|
+
if (owed) {
|
|
604
|
+
nextDelay = FAILURE_RETRY_MS;
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
nextDelay = Math.min(FAILURE_RETRY_MS * 2 ** failures, intervalMs);
|
|
608
|
+
failures += 1;
|
|
609
|
+
}
|
|
610
|
+
/** When the armed timer fires; Infinity when none is armed. */
|
|
611
|
+
let dueAt = Number.POSITIVE_INFINITY;
|
|
612
|
+
/** A change landed while a cycle was running: that cycle may have read the disk before it. */
|
|
613
|
+
let dirty = false;
|
|
344
614
|
const schedule = (delay) => {
|
|
345
|
-
if (stopped)
|
|
615
|
+
if (stopped) {
|
|
346
616
|
return;
|
|
617
|
+
}
|
|
347
618
|
// Replace, never stack. `nudge` and the interval both schedule, and leaving the old timer armed
|
|
348
619
|
// would let every nudge add a permanent extra cycle per minute for the life of the process.
|
|
349
|
-
if (timer !== undefined)
|
|
620
|
+
if (timer !== undefined) {
|
|
350
621
|
clearTimeout(timer);
|
|
622
|
+
}
|
|
623
|
+
dueAt = now() + delay;
|
|
351
624
|
timer = setTimeout(() => {
|
|
352
|
-
|
|
625
|
+
dueAt = Number.POSITIVE_INFINITY;
|
|
626
|
+
void cycle().finally(() => {
|
|
627
|
+
schedule(nextDelay);
|
|
628
|
+
afterCycle();
|
|
629
|
+
});
|
|
353
630
|
}, delay);
|
|
354
631
|
// Unref'd: a pending sync must never be the reason a process refuses to exit.
|
|
355
632
|
timer.unref?.();
|
|
356
633
|
};
|
|
634
|
+
/** Cycle within `delay`, but never LATER than a cycle already armed. */
|
|
635
|
+
const soon = (delay) => {
|
|
636
|
+
if (now() + delay < dueAt) {
|
|
637
|
+
schedule(delay);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
/** One follow-up for the changes a cycle may have missed, however many there were. */
|
|
641
|
+
const afterCycle = () => {
|
|
642
|
+
if (!dirty) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
dirty = false;
|
|
646
|
+
soon(CHANGE_DEBOUNCE_MS);
|
|
647
|
+
};
|
|
648
|
+
const onChange = (path) => {
|
|
649
|
+
if (stopped || isOwnWrite(path)) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (isIntent(path)) {
|
|
653
|
+
intentMoved = true;
|
|
654
|
+
}
|
|
655
|
+
owed = true;
|
|
656
|
+
if (running) {
|
|
657
|
+
dirty = true;
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
soon(CHANGE_DEBOUNCE_MS);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
let unwatch;
|
|
664
|
+
/** Watch the root once it exists; a root created mid-session is picked up after the next cycle. */
|
|
665
|
+
const startWatching = () => {
|
|
666
|
+
if (stopped || unwatch !== undefined || deps.watch === undefined) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
unwatch = deps.watch(onChange);
|
|
670
|
+
};
|
|
671
|
+
const stopWatching = () => {
|
|
672
|
+
unwatch?.();
|
|
673
|
+
unwatch = undefined;
|
|
674
|
+
};
|
|
357
675
|
schedule(FIRST_CYCLE_DELAY_MS);
|
|
358
|
-
|
|
359
|
-
const
|
|
676
|
+
startWatching();
|
|
677
|
+
const poll = setInterval(() => void pollMemory(), MEMORY_POLL_TICK_MS);
|
|
678
|
+
poll.unref?.();
|
|
360
679
|
return {
|
|
361
680
|
// A sync somebody asked for writes what is held on a timer first: a drive's run waits a few
|
|
362
681
|
// seconds after its last verdict, and a push straight after `session end` sent "0 of 0".
|
|
363
682
|
syncNow: async () => {
|
|
364
683
|
await flushPendingWrites();
|
|
365
|
-
|
|
684
|
+
const report = await cycle(true);
|
|
685
|
+
afterCycle();
|
|
686
|
+
return report;
|
|
366
687
|
},
|
|
367
688
|
unsyncedRoots: () => unsynced,
|
|
689
|
+
background: () => {
|
|
690
|
+
const gate = gates.get(deps.reticleRoot);
|
|
691
|
+
return {
|
|
692
|
+
state: gate?.state ?? BackgroundSyncState.ON,
|
|
693
|
+
why: gate?.why ?? 'not checked yet',
|
|
694
|
+
failures,
|
|
695
|
+
...(reportedError === undefined ? {} : { lastError: reportedError }),
|
|
696
|
+
...(lastPushAt === undefined ? {} : { lastPushAt }),
|
|
697
|
+
};
|
|
698
|
+
},
|
|
368
699
|
nudge: () => {
|
|
369
700
|
// A cycle already in flight will not pick this up, so the nudge is still scheduled behind it
|
|
370
701
|
// rather than dropped — otherwise the run that arrived during a slow cycle waits a full tick.
|
|
371
702
|
schedule(NUDGE_DELAY_MS);
|
|
372
703
|
},
|
|
373
704
|
stop: () => {
|
|
374
|
-
|
|
705
|
+
stopWatching();
|
|
706
|
+
clearInterval(poll);
|
|
375
707
|
stopped = true;
|
|
376
|
-
if (timer !== undefined)
|
|
708
|
+
if (timer !== undefined) {
|
|
377
709
|
clearTimeout(timer);
|
|
710
|
+
}
|
|
378
711
|
timer = undefined;
|
|
379
712
|
},
|
|
380
713
|
flush: async () => {
|
|
381
|
-
|
|
714
|
+
stopWatching();
|
|
715
|
+
clearInterval(poll);
|
|
382
716
|
stopped = true;
|
|
383
|
-
if (timer !== undefined)
|
|
717
|
+
if (timer !== undefined) {
|
|
384
718
|
clearTimeout(timer);
|
|
719
|
+
}
|
|
385
720
|
timer = undefined;
|
|
386
721
|
// A cycle already running may have started BEFORE the write being flushed, so its report says
|
|
387
722
|
// nothing about that artifact. Wait it out rather than skipping on the overlap guard.
|
|
388
|
-
const deadline =
|
|
389
|
-
while (running &&
|
|
723
|
+
const deadline = now() + FLUSH_BUDGET_MS;
|
|
724
|
+
while (running && now() < deadline) {
|
|
390
725
|
await new Promise((resolve) => setTimeout(resolve, FLUSH_POLL_MS));
|
|
391
|
-
|
|
726
|
+
}
|
|
727
|
+
// Not `asked`: on the way out, the run goes first; the memory and the other repos only if due.
|
|
728
|
+
// Bounded by what is left of the budget: a cycle can wait on a network that does not answer.
|
|
729
|
+
let lateTimer;
|
|
730
|
+
const late = new Promise((resolve) => {
|
|
731
|
+
lateTimer = setTimeout(resolve, Math.max(0, deadline - now()));
|
|
732
|
+
});
|
|
733
|
+
await Promise.race([cycle().then(() => undefined), late]);
|
|
734
|
+
clearTimeout(lateTimer);
|
|
392
735
|
},
|
|
393
736
|
};
|
|
394
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* Flush, but return after `budgetMs` at the latest, and never throw.
|
|
740
|
+
*
|
|
741
|
+
* For a process on its way out. The cycle has per-call timeouts, but a stalled network still adds
|
|
742
|
+
* them up, and an exit must not wait on that. A cycle cut short loses nothing: the artifacts stay
|
|
743
|
+
* on disk, and the next daemon or `reticle sync` sends them.
|
|
744
|
+
*/
|
|
745
|
+
export async function flushWithin(daemon, budgetMs) {
|
|
746
|
+
let timer;
|
|
747
|
+
const deadline = new Promise((resolve) => {
|
|
748
|
+
timer = setTimeout(resolve, budgetMs);
|
|
749
|
+
timer.unref?.();
|
|
750
|
+
});
|
|
751
|
+
await Promise.race([daemon.flush().catch(() => undefined), deadline]);
|
|
752
|
+
clearTimeout(timer);
|
|
753
|
+
}
|
|
395
754
|
/**
|
|
396
755
|
* Start cloud sync for a daemon and hand back the two hooks the daemon wires to it.
|
|
397
756
|
*
|
|
@@ -405,30 +764,61 @@ export function startSyncDaemon(deps) {
|
|
|
405
764
|
* resolving a credential — and the part a bootstrap file has no business spelling out.
|
|
406
765
|
*/
|
|
407
766
|
export function attachCloudSync(deps) {
|
|
767
|
+
const signedIn = deps.signedIn;
|
|
408
768
|
return startSyncDaemon({
|
|
409
769
|
reticleRoot: deps.reticleRoot,
|
|
770
|
+
clientVersion: deps.clientVersion,
|
|
410
771
|
cloud: () => resolveProjectCloud(deps.fs, deps.reticleRoot, deps.homeDir, deps.env),
|
|
411
772
|
// Every OTHER linked repo on this machine, not just the directory the daemon was started in.
|
|
412
773
|
// One daemon serves many projects, and pushing only its own root left the rest silently
|
|
413
774
|
// reporting nothing — indistinguishable, on the dashboard, from nobody having verified anything.
|
|
414
775
|
otherRoots: () => Promise.resolve(deps.otherRoots()),
|
|
415
776
|
cloudFor: (root) => resolveProjectCloud(deps.fs, root, deps.homeDir, deps.env),
|
|
416
|
-
// ponytail: this root only;
|
|
417
|
-
|
|
777
|
+
// ponytail: this root only; sibling roots sync at most once a minute, when this one changes,
|
|
778
|
+
// and on the backstop timer.
|
|
779
|
+
// Watching them too means one recursive watcher per linked repo — add it if siblings lag.
|
|
780
|
+
watch: (onChange) => watchReticleRoot(deps.reticleRoot, onChange),
|
|
781
|
+
// Sent only while somebody is signed in, to a linked project that did not switch it off.
|
|
782
|
+
...(signedIn === undefined
|
|
783
|
+
? {}
|
|
784
|
+
: {
|
|
785
|
+
gate: (root, cloud) => backgroundSyncGate({ reticleRoot: root, cloud, env: deps.env, signedIn }),
|
|
786
|
+
}),
|
|
787
|
+
...(deps.request === undefined ? {} : { request: deps.request }),
|
|
788
|
+
...(deps.intervalMs === undefined ? {} : { intervalMs: deps.intervalMs }),
|
|
418
789
|
});
|
|
419
790
|
}
|
|
420
|
-
/**
|
|
421
|
-
|
|
791
|
+
/**
|
|
792
|
+
* Watch a whole `.reticle` root, reporting each change by its path under the root.
|
|
793
|
+
*
|
|
794
|
+
* Returns undefined only when the root does not exist yet, so the caller tries again later. Where a
|
|
795
|
+
* recursive watch is unsupported (an old Node on Linux, a network filesystem) or fails later (too
|
|
796
|
+
* many files), it says so ONCE and returns a no-op: the backstop timer is the whole sync from then on.
|
|
797
|
+
*/
|
|
798
|
+
export function watchReticleRoot(reticleRoot, onChange, watchFn = (root, listener) => watch(root, { recursive: true }, listener)) {
|
|
799
|
+
const unavailable = (error) => log('reticle_cloud_watch_unavailable', {
|
|
800
|
+
root: reticleRoot,
|
|
801
|
+
error: error instanceof Error ? error.message : String(error),
|
|
802
|
+
fallback: 'syncing on the timer only',
|
|
803
|
+
});
|
|
422
804
|
try {
|
|
423
|
-
const watcher =
|
|
424
|
-
if (
|
|
425
|
-
onChange();
|
|
805
|
+
const watcher = watchFn(reticleRoot, (_event, name) => {
|
|
806
|
+
if (name !== null) {
|
|
807
|
+
onChange(name);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
watcher.on('error', (error) => {
|
|
811
|
+
unavailable(error);
|
|
812
|
+
watcher.close();
|
|
426
813
|
});
|
|
427
|
-
watcher.on('error', () => watcher.close());
|
|
428
814
|
watcher.unref();
|
|
429
815
|
return () => watcher.close();
|
|
430
816
|
}
|
|
431
|
-
catch {
|
|
817
|
+
catch (error) {
|
|
818
|
+
if ('ENOENT' === error.code) {
|
|
819
|
+
return undefined;
|
|
820
|
+
}
|
|
821
|
+
unavailable(error);
|
|
432
822
|
return () => undefined;
|
|
433
823
|
}
|
|
434
824
|
}
|