@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
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import * as http from 'node:http';
|
|
2
|
-
import {
|
|
2
|
+
import { stateDirProblem } from '../../command/daemon/state-dir.js';
|
|
3
|
+
import { reticleStateHome } from '../../command/daemon/daemon.js';
|
|
4
|
+
import { localInitializeResponse, degradedInstructions, isHandshakeLine, drainLines, MAX_STDIN_LINE_BYTES, parseLine as parseJsonRpc, HandshakeReplay, } from './proxy/proxy-handshake.js';
|
|
5
|
+
export { HandshakeReplay, RECONNECT_INITIALIZE_ID } from './proxy/proxy-handshake.js';
|
|
3
6
|
import { isToolCallRequest, isToolsListRequest, ToolCatalogCache } from './tool-catalog-cache.js';
|
|
4
7
|
import { rememberEnumerated, rememberProxyStarted, rememberToolCalled } from './attach-memory.js';
|
|
5
8
|
import { toolsChangedNotification } from './proxy/proxy-handshake.js';
|
|
6
|
-
import { LOOPBACK_HOST, MCP_SSE_PATH, MCP_SHUTDOWN_EVENT, ReticleEnv, CONTRACT_FINGERPRINT, } from '@reticlehq/core';
|
|
7
|
-
import { SseFrameParser } from './sse-frame-parser.js';
|
|
8
|
-
export { SseFrameParser } from './sse-frame-parser.js';
|
|
9
|
-
export { probeDaemon, waitForDaemon, waitForDaemonBind } from './proxy/proxy-daemon-probe.js';
|
|
9
|
+
import { LOOPBACK_HOST, MCP_SSE_PATH, MCP_SHUTDOWN_EVENT, ReticleEnv, CONTRACT_FINGERPRINT, ProxyExitReason, PROXY_ID_PARAM, } from '@reticlehq/core';
|
|
10
|
+
import { SseFrameParser, buildSessionUrl } from './sse-frame-parser.js';
|
|
11
|
+
export { SseFrameParser, buildSessionUrl } from './sse-frame-parser.js';
|
|
12
|
+
export { daemonProcessAlive, probeDaemon, waitForDaemon, waitForDaemonBind, } from './proxy/proxy-daemon-probe.js';
|
|
10
13
|
import { probeDaemon } from './proxy/proxy-daemon-probe.js';
|
|
11
14
|
import { SERVER_VERSION } from '../../command/version/identity/server-version.js';
|
|
12
15
|
import { buildServerInstructions, localizeInstructions } from './server-instructions.js';
|
|
13
16
|
import { hasAnyProjectConnectedBefore } from '../../memory/recall/prior/connection-memory.js';
|
|
14
|
-
import { reticleStateHome } from '../../command/daemon/daemon.js';
|
|
15
17
|
import { projectIdsAt } from '../../command/cli/ports/resolve/cli-port.js';
|
|
16
|
-
import { PEER_VERSION_PARAM, PEER_CONTRACT_PARAM } from '../../command/version/peer-announce.js';
|
|
18
|
+
import { PEER_VERSION_PARAM, PEER_CONTRACT_PARAM, agentSessionQuery, } from '../../command/version/peer-announce.js';
|
|
17
19
|
import { onStreamDrop, onClientRequest, onReconnectBudgetSpent, onQueueExpired, OnDrop, OnRequest, } from './proxy/proxy-lifecycle.js';
|
|
18
20
|
import { describePresence, probePresence } from '../../command/daemon/binding/port-presence.js';
|
|
19
|
-
import {
|
|
21
|
+
import { PROXY_ID, flushProxyExit, noteProxyDrop, noteProxyToolCall, } from '../../telemetry/proxy-telemetry.js';
|
|
22
|
+
export { flushProxyExit, reportWakeRefused } from '../../telemetry/proxy-telemetry.js';
|
|
20
23
|
import { detectStack } from '../../telemetry/feedback-context.js';
|
|
21
24
|
/**
|
|
22
25
|
* The same `/status` probe `doctor`, `status` and `kill` ask with. Reused rather than re-written:
|
|
@@ -32,7 +35,8 @@ export { accountProxyLogWrite, proxyLog, proxyLogPath, PROXY_LOG_CHECK_BYTES, re
|
|
|
32
35
|
import { proxyLog } from './proxy/proxy-log.js';
|
|
33
36
|
import { OutageReason, OutageStage, reportMcpOutage } from './faults/mcp-outage.js';
|
|
34
37
|
import { postToSession } from './proxy/mcp-post-transport.js';
|
|
35
|
-
import {
|
|
38
|
+
import { PendingRequests, streamLossReplies, transportLossReply } from './proxy/drive-calls.js';
|
|
39
|
+
export { PendingRequests, streamLossReplies } from './proxy/drive-calls.js';
|
|
36
40
|
import { reconnectDelayMs } from './proxy/proxy-backoff.js';
|
|
37
41
|
import { PROXY_IDLE_EXIT_EVENT, ProxyIdleExit, resolveProxyIdleExitMs } from './proxy-idle-exit.js';
|
|
38
42
|
export { reconnectDelayMs } from './proxy/proxy-backoff.js';
|
|
@@ -55,18 +59,6 @@ export const MAX_RECONNECT_ATTEMPTS = (() => {
|
|
|
55
59
|
const raw = Number(process.env[ReticleEnv.RECONNECT_ATTEMPTS]);
|
|
56
60
|
return Number.isInteger(raw) && raw > 0 ? raw : DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
57
61
|
})();
|
|
58
|
-
/** JSON-RPC id the proxy uses for its own replayed `initialize` — never one the client could send. */
|
|
59
|
-
export const RECONNECT_INITIALIZE_ID = '__reticle_proxy_reinit';
|
|
60
|
-
function parseJsonRpc(line) {
|
|
61
|
-
try {
|
|
62
|
-
const parsed = JSON.parse(line);
|
|
63
|
-
return 'object' === typeof parsed && parsed !== null ? parsed : null;
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const INITIALIZE_METHOD = 'initialize';
|
|
70
62
|
/**
|
|
71
63
|
* How long the handshake waits on the daemon before the proxy answers it.
|
|
72
64
|
*
|
|
@@ -75,105 +67,6 @@ const INITIALIZE_METHOD = 'initialize';
|
|
|
75
67
|
* past 25s with nothing on stderr, and the reported client gave up at 60s having run no tools.
|
|
76
68
|
*/
|
|
77
69
|
const LOCAL_HANDSHAKE_MS = 12_000;
|
|
78
|
-
const INITIALIZED_METHOD = 'notifications/initialized';
|
|
79
|
-
/**
|
|
80
|
-
* JSON-RPC requests forwarded on behalf of the client that have not been answered yet.
|
|
81
|
-
*
|
|
82
|
-
* Only used at shutdown, where it is the difference between "the client closed stdin" and "the client
|
|
83
|
-
* closed stdin and we dropped its `tools/list` on the floor while reporting exit 0". A request has an
|
|
84
|
-
* id AND a method; a notification has no id and is owed nothing; a response has an id and no method.
|
|
85
|
-
*/
|
|
86
|
-
export class PendingRequests {
|
|
87
|
-
#ids = new Set();
|
|
88
|
-
/** The pending requests that start or poll a Harness drive, which outlives a lost connection. */
|
|
89
|
-
#drives = new Set();
|
|
90
|
-
observeOutbound(line) {
|
|
91
|
-
const msg = parseJsonRpc(line);
|
|
92
|
-
if (null === msg || msg.id === undefined || msg.method === undefined)
|
|
93
|
-
return;
|
|
94
|
-
const id = JSON.stringify(msg.id);
|
|
95
|
-
this.#ids.add(id);
|
|
96
|
-
if (isHarnessDriveCall(msg))
|
|
97
|
-
this.#drives.add(id);
|
|
98
|
-
}
|
|
99
|
-
/** Whether this unanswered request asked for a Harness drive. */
|
|
100
|
-
isDrive(id) {
|
|
101
|
-
return this.#drives.has(JSON.stringify(id));
|
|
102
|
-
}
|
|
103
|
-
observeInbound(line) {
|
|
104
|
-
const msg = parseJsonRpc(line);
|
|
105
|
-
if (null === msg || msg.id === undefined)
|
|
106
|
-
return;
|
|
107
|
-
this.#ids.delete(JSON.stringify(msg.id));
|
|
108
|
-
}
|
|
109
|
-
get unanswered() {
|
|
110
|
-
return [...this.#ids];
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Claim ONE unanswered id, so exactly one path may answer it. False means somebody already did —
|
|
114
|
-
* a duplicate response to one id is a protocol violation, not a belt-and-braces retry.
|
|
115
|
-
*/
|
|
116
|
-
take(id) {
|
|
117
|
-
this.#drives.delete(JSON.stringify(id));
|
|
118
|
-
return this.#ids.delete(JSON.stringify(id));
|
|
119
|
-
}
|
|
120
|
-
/** Take every unanswered id and forget them, so the same request is failed at most once. */
|
|
121
|
-
drain() {
|
|
122
|
-
const ids = [...this.#ids];
|
|
123
|
-
this.#ids.clear();
|
|
124
|
-
return ids;
|
|
125
|
-
}
|
|
126
|
-
/** Forget which requests were drives, once their replies are written. */
|
|
127
|
-
clearDrives() {
|
|
128
|
-
this.#drives.clear();
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* JSON-RPC errors for every request that was in flight when the stream died.
|
|
133
|
-
*
|
|
134
|
-
* These are unrecoverable BY CONSTRUCTION: the daemon builds a fresh MCP session per SSE
|
|
135
|
-
* connection, so the reconnected session has never seen them. Re-sending them is not an option
|
|
136
|
-
* either — a `reticle_act` that already clicked would click twice, and inventing a second action in
|
|
137
|
-
* somebody's app is worse than reporting a failure.
|
|
138
|
-
*
|
|
139
|
-
* So the proxy answers them itself, under each request's own id. Found by brute force: killing the
|
|
140
|
-
* daemon under a live client left 20 of 20 concurrent calls hanging until the client's own timeout,
|
|
141
|
-
* with the MCP server still perfectly alive. An agent cannot tell that apart from "still working".
|
|
142
|
-
*/
|
|
143
|
-
export function streamLossReplies(pending, reason, nextStep) {
|
|
144
|
-
const replies = pending.drain().map((id) => {
|
|
145
|
-
const parsed = JSON.parse(id);
|
|
146
|
-
return transportLossReply(parsed, reason, nextStep, pending.isDrive(parsed));
|
|
147
|
-
});
|
|
148
|
-
pending.clearDrives();
|
|
149
|
-
return replies;
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* -32001: a server-defined error. Not -32603 (internal): nothing went wrong INSIDE the call, the
|
|
153
|
-
* transport under it went away, and the distinction is what tells a caller the action may be safe
|
|
154
|
-
* to retry.
|
|
155
|
-
*/
|
|
156
|
-
const TRANSPORT_LOSS_CODE = -32001;
|
|
157
|
-
/**
|
|
158
|
-
* The one shape every "the transport ate your call" answer takes, whichever leg died.
|
|
159
|
-
*
|
|
160
|
-
* `nextStep` is appended when the caller knows one. Without it this reply describes a condition and
|
|
161
|
-
* stops there, which is all a first-run user got out of their first ever tool call.
|
|
162
|
-
*/
|
|
163
|
-
function transportLossReply(id, reason, nextStep, drive = false) {
|
|
164
|
-
return JSON.stringify({
|
|
165
|
-
jsonrpc: '2.0',
|
|
166
|
-
id,
|
|
167
|
-
error: {
|
|
168
|
-
code: TRANSPORT_LOSS_CODE,
|
|
169
|
-
message: (drive
|
|
170
|
-
? `the daemon connection dropped (${reason}) before this call was answered. ${MSG_DRIVE_MAY_BE_RUNNING}`
|
|
171
|
-
: `the daemon connection dropped (${reason}) before this call was answered — it did NOT ` +
|
|
172
|
-
'complete. Reticle is recovering the connection; retry if the action is safe to repeat.') +
|
|
173
|
-
(nextStep === undefined ? '' : ` ${nextStep}`),
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
70
|
/**
|
|
178
71
|
* How long stdin closing gives the daemon to answer what is still in flight.
|
|
179
72
|
*
|
|
@@ -223,72 +116,8 @@ export function shouldRetryFirstConnect(error, elapsedMs) {
|
|
|
223
116
|
*/
|
|
224
117
|
export function queueExpiryNextStep(port) {
|
|
225
118
|
return (`No Reticle daemon answered on port ${String(port)}. Retry the call — the next request starts ` +
|
|
226
|
-
'one. If it keeps failing, run `
|
|
227
|
-
'port and whether the daemon can start at all.');
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Makes a reconnect invisible to the client.
|
|
231
|
-
*
|
|
232
|
-
* The daemon builds a FRESH `McpServer` per SSE connection, so a reconnected session has never seen
|
|
233
|
-
* the client's `initialize` — every subsequent `tools/call` would hit an uninitialized server. The
|
|
234
|
-
* proxy therefore remembers the handshake the client sent once and replays it into each new session.
|
|
235
|
-
* The replay is re-issued under a reserved id so the daemon's response to it can be dropped rather
|
|
236
|
-
* than reaching the client's stdout, where a duplicate JSON-RPC id would be a protocol violation.
|
|
237
|
-
*/
|
|
238
|
-
export class HandshakeReplay {
|
|
239
|
-
#initialize = null;
|
|
240
|
-
#initialized = null;
|
|
241
|
-
#pendingReplayId = null;
|
|
242
|
-
/** Record one outbound client line. Cheap enough to call for every stdin message. */
|
|
243
|
-
observeOutbound(line) {
|
|
244
|
-
const msg = parseJsonRpc(line);
|
|
245
|
-
if (null === msg)
|
|
246
|
-
return;
|
|
247
|
-
// The first initialize is the session's identity; a client re-issuing one does not change it.
|
|
248
|
-
if (msg.method === INITIALIZE_METHOD && null === this.#initialize)
|
|
249
|
-
this.#initialize = line;
|
|
250
|
-
else if (msg.method === INITIALIZED_METHOD)
|
|
251
|
-
this.#initialized = line;
|
|
252
|
-
}
|
|
253
|
-
/** Lines to POST into a freshly-established session, before any queued client traffic. */
|
|
254
|
-
replayLines() {
|
|
255
|
-
if (null === this.#initialize)
|
|
256
|
-
return [];
|
|
257
|
-
const msg = parseJsonRpc(this.#initialize);
|
|
258
|
-
if (null === msg)
|
|
259
|
-
return [];
|
|
260
|
-
this.#pendingReplayId = RECONNECT_INITIALIZE_ID;
|
|
261
|
-
const reinit = { ...msg, id: RECONNECT_INITIALIZE_ID };
|
|
262
|
-
return null === this.#initialized
|
|
263
|
-
? [JSON.stringify(reinit)]
|
|
264
|
-
: [JSON.stringify(reinit), this.#initialized];
|
|
265
|
-
}
|
|
266
|
-
/** True when this inbound daemon line is the echo of our replayed handshake and must not be forwarded. */
|
|
267
|
-
shouldSuppressInbound(line) {
|
|
268
|
-
if (null === this.#pendingReplayId)
|
|
269
|
-
return false;
|
|
270
|
-
const msg = parseJsonRpc(line);
|
|
271
|
-
if (null === msg || msg.id !== this.#pendingReplayId)
|
|
272
|
-
return false;
|
|
273
|
-
this.#pendingReplayId = null; // one response per replay — anything later is not ours to swallow
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Turn an `endpoint` frame's data into a POST target, or null when there is nothing usable in it.
|
|
279
|
-
*
|
|
280
|
-
* An empty (or whitespace) data field is the shape a malformed daemon response takes, and it used to
|
|
281
|
-
* be accepted: `postUrl` became `''`, which is not `null`, so the proxy considered itself CONNECTED
|
|
282
|
-
* and posted every subsequent message to the empty string. Each one failed, the queue never armed
|
|
283
|
-
* its deadline because nothing was ever queued, and the agent held a session that could not carry a
|
|
284
|
-
* single call. Refusing the frame keeps the proxy in its disconnected path, which already knows how
|
|
285
|
-
* to reconnect and how to answer what it cannot send.
|
|
286
|
-
*/
|
|
287
|
-
export function buildSessionUrl(rawData, port) {
|
|
288
|
-
const trimmed = rawData.trim();
|
|
289
|
-
if ('' === trimmed)
|
|
290
|
-
return null;
|
|
291
|
-
return trimmed.startsWith('/') ? `http://${LOOPBACK_HOST}:${port}${trimmed}` : trimmed;
|
|
119
|
+
'one. If it keeps failing, run `npx @reticlehq/server doctor` in a terminal: it reports what ' +
|
|
120
|
+
'is holding the port and whether the daemon can start at all.');
|
|
292
121
|
}
|
|
293
122
|
/**
|
|
294
123
|
* Bridge stdio ↔ SSE: connects to the running daemon's MCP endpoint and forwards
|
|
@@ -307,9 +136,12 @@ export function buildSessionUrl(rawData, port) {
|
|
|
307
136
|
* an unreadable state home must never be the reason a handshake fails, so it degrades to the
|
|
308
137
|
* fresh-install wording rather than throwing.
|
|
309
138
|
*/
|
|
310
|
-
function proxyInstructions(port) {
|
|
139
|
+
function proxyInstructions(port, advertised) {
|
|
311
140
|
try {
|
|
312
|
-
return buildServerInstructions(
|
|
141
|
+
return buildServerInstructions({
|
|
142
|
+
...instructionStateAt(port),
|
|
143
|
+
...(advertised === undefined ? {} : { advertised }),
|
|
144
|
+
});
|
|
313
145
|
}
|
|
314
146
|
catch {
|
|
315
147
|
return buildServerInstructions({ previouslyConnected: false });
|
|
@@ -325,32 +157,42 @@ export function instructionStateAt(port, liveSessions = 0) {
|
|
|
325
157
|
}
|
|
326
158
|
/** An `initialize` reply re-briefed for the AGENT's directory; see localizeInstructions. */
|
|
327
159
|
function withLocalBriefing(line, appHere) {
|
|
328
|
-
if (!line.includes('"instructions"'))
|
|
160
|
+
if (!line.includes('"instructions"')) {
|
|
329
161
|
return line;
|
|
162
|
+
}
|
|
330
163
|
try {
|
|
331
164
|
const msg = JSON.parse(line);
|
|
332
|
-
if (null === msg || 'object' !== typeof msg || !('result' in msg))
|
|
165
|
+
if (null === msg || 'object' !== typeof msg || !('result' in msg)) {
|
|
333
166
|
return line;
|
|
167
|
+
}
|
|
334
168
|
const result = msg.result;
|
|
335
|
-
if (null === result || 'object' !== typeof result || !('instructions' in result))
|
|
169
|
+
if (null === result || 'object' !== typeof result || !('instructions' in result)) {
|
|
336
170
|
return line;
|
|
171
|
+
}
|
|
337
172
|
const { instructions } = result;
|
|
338
|
-
if ('string' !== typeof instructions)
|
|
173
|
+
if ('string' !== typeof instructions) {
|
|
339
174
|
return line;
|
|
175
|
+
}
|
|
340
176
|
const localized = localizeInstructions(instructions, appHere);
|
|
341
|
-
if (localized === instructions)
|
|
177
|
+
if (localized === instructions) {
|
|
342
178
|
return line;
|
|
179
|
+
}
|
|
343
180
|
return JSON.stringify({ ...msg, result: { ...result, instructions: localized } });
|
|
344
181
|
}
|
|
345
182
|
catch {
|
|
346
183
|
return line;
|
|
347
184
|
}
|
|
348
185
|
}
|
|
349
|
-
|
|
186
|
+
function withStateDirDiagnostic(reason) {
|
|
187
|
+
const problem = stateDirProblem(reticleStateHome());
|
|
188
|
+
return problem === undefined ? reason : `${reason}\n${problem}`;
|
|
189
|
+
}
|
|
190
|
+
export function startMcpProxy(bootPort,
|
|
350
191
|
/**
|
|
351
192
|
* Bring a daemon back before reconnecting. Without it the proxy retried a DEAD port until the
|
|
352
193
|
* budget ran out and then killed itself, taking the agent's whole Reticle surface with it — the
|
|
353
|
-
* failure mode for a daemon that crashed, was stopped, or shut itself down as idle.
|
|
194
|
+
* failure mode for a daemon that crashed, was stopped, or shut itself down as idle. A returned
|
|
195
|
+
* port is where the daemon now is; the proxy dials it from then on.
|
|
354
196
|
*/
|
|
355
197
|
ensureDaemon,
|
|
356
198
|
/** Observe the real shutdown boundary in transport tests without terminating their worker. */
|
|
@@ -363,7 +205,13 @@ exitProcess = (code) => process.exit(code),
|
|
|
363
205
|
* drops it, and an `endpoint` frame is not. Optional, because a transport test drives this
|
|
364
206
|
* function with no resilience installed.
|
|
365
207
|
*/
|
|
366
|
-
onUsableSession = () => undefined
|
|
208
|
+
onUsableSession = () => undefined,
|
|
209
|
+
/**
|
|
210
|
+
* The daemon start that runs while the proxy already reads stdin. A client that does not wait
|
|
211
|
+
* for MCP servers gets the handshake and the tool list before the daemon is up.
|
|
212
|
+
*/
|
|
213
|
+
boot) {
|
|
214
|
+
let port = bootPort;
|
|
367
215
|
return new Promise((_resolve, reject) => {
|
|
368
216
|
// A client just started us, which is the only honest evidence that Reticle is registered with
|
|
369
217
|
// one at all: a config entry proves somebody wrote JSON, not that a client ever read it. Paired
|
|
@@ -401,8 +249,9 @@ onUsableSession = () => undefined) {
|
|
|
401
249
|
*/
|
|
402
250
|
let queueTimer;
|
|
403
251
|
const clearQueueTimer = () => {
|
|
404
|
-
if (queueTimer !== undefined)
|
|
252
|
+
if (queueTimer !== undefined) {
|
|
405
253
|
clearTimeout(queueTimer);
|
|
254
|
+
}
|
|
406
255
|
queueTimer = undefined;
|
|
407
256
|
};
|
|
408
257
|
/**
|
|
@@ -415,8 +264,9 @@ onUsableSession = () => undefined) {
|
|
|
415
264
|
*/
|
|
416
265
|
const expireQueue = (reason) => {
|
|
417
266
|
clearQueueTimer();
|
|
418
|
-
if (stopped || 0 === stdinQueue.length)
|
|
267
|
+
if (stopped || 0 === stdinQueue.length) {
|
|
419
268
|
return;
|
|
269
|
+
}
|
|
420
270
|
// Drop what we could never send, and answer it, so the caller is never left guessing.
|
|
421
271
|
stdinQueue.splice(0);
|
|
422
272
|
for (const reply of streamLossReplies(pending, reason, queueExpiryNextStep(port))) {
|
|
@@ -436,8 +286,9 @@ onUsableSession = () => undefined) {
|
|
|
436
286
|
});
|
|
437
287
|
};
|
|
438
288
|
const armQueueTimer = () => {
|
|
439
|
-
if (queueTimer !== undefined)
|
|
440
|
-
return;
|
|
289
|
+
if (queueTimer !== undefined) {
|
|
290
|
+
return;
|
|
291
|
+
} // the oldest entry owns the deadline
|
|
441
292
|
queueTimer = setTimeout(() => {
|
|
442
293
|
queueTimer = undefined;
|
|
443
294
|
expireQueue('no daemon could be reached');
|
|
@@ -447,13 +298,14 @@ onUsableSession = () => undefined) {
|
|
|
447
298
|
const replay = new HandshakeReplay();
|
|
448
299
|
const appHere = detectStack(process.cwd()).stack !== undefined;
|
|
449
300
|
const pending = new PendingRequests();
|
|
450
|
-
const quit = (code) => {
|
|
451
|
-
if (stopped)
|
|
301
|
+
const quit = (code, reason) => {
|
|
302
|
+
if (stopped) {
|
|
452
303
|
return;
|
|
304
|
+
}
|
|
453
305
|
stopped = true;
|
|
454
306
|
idleExit.stop();
|
|
455
307
|
// Await: fire-and-forget here is how daemon_stopped never arrived.
|
|
456
|
-
void
|
|
308
|
+
void flushProxyExit(reason).finally(() => exitProcess(code));
|
|
457
309
|
};
|
|
458
310
|
const idleExit = new ProxyIdleExit({
|
|
459
311
|
graceMs: resolveProxyIdleExitMs(process.env[ReticleEnv.MCP_PROXY_IDLE]),
|
|
@@ -464,7 +316,7 @@ onUsableSession = () => undefined) {
|
|
|
464
316
|
idleMs,
|
|
465
317
|
note: 'the client link was quiet for the configured idle window with no work in flight; exiting the abandoned proxy',
|
|
466
318
|
});
|
|
467
|
-
quit(0);
|
|
319
|
+
quit(0, ProxyExitReason.IDLE);
|
|
468
320
|
},
|
|
469
321
|
});
|
|
470
322
|
idleExit.start();
|
|
@@ -482,12 +334,9 @@ onUsableSession = () => undefined) {
|
|
|
482
334
|
/**
|
|
483
335
|
* What the outage had already cost when the proxy went dormant.
|
|
484
336
|
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
* evidence that there was anything to recover FROM — and dormancy is the commonest outage class
|
|
489
|
-
* we have, which made every recovery of that class invisible. Carried across the wake so the
|
|
490
|
-
* budget resets and the accounting does not.
|
|
337
|
+
* `wake` clears `attempts`, because a wake gets a fresh retry budget. The recovery report needs
|
|
338
|
+
* `attempts > 0`, so the clear also hid every recovery from dormancy, the most common outage.
|
|
339
|
+
* This count goes across the wake: the budget resets, and the accounting does not.
|
|
491
340
|
*/
|
|
492
341
|
let attemptsBeforeDormant = 0;
|
|
493
342
|
/**
|
|
@@ -507,12 +356,14 @@ onUsableSession = () => undefined) {
|
|
|
507
356
|
*/
|
|
508
357
|
const forward = (url, line) => {
|
|
509
358
|
void postToSession(url, line).then((failure) => {
|
|
510
|
-
if (null === failure)
|
|
359
|
+
if (null === failure) {
|
|
511
360
|
return;
|
|
361
|
+
}
|
|
512
362
|
const msg = parseJsonRpc(line);
|
|
513
363
|
const drive = null !== msg && pending.isDrive(msg.id);
|
|
514
|
-
if (null === msg || msg.id === undefined || !pending.take(msg.id))
|
|
364
|
+
if (null === msg || msg.id === undefined || !pending.take(msg.id)) {
|
|
515
365
|
return;
|
|
366
|
+
}
|
|
516
367
|
// No outage is reported here, deliberately. A POST leg that died while the SSE stream is
|
|
517
368
|
// still up is not the agent losing its tools: `postSocketFailures` on the session summary
|
|
518
369
|
// already counts it, and reporting it as a lost stream both double-counted and — because
|
|
@@ -545,8 +396,9 @@ onUsableSession = () => undefined) {
|
|
|
545
396
|
// Report the recovery BEFORE the counter is cleared: `attempts` is what coming back actually
|
|
546
397
|
// cost, and it is the only number that can falsify the drop event. See OutageStage.RECOVERED.
|
|
547
398
|
const cost = attempts + attemptsBeforeDormant;
|
|
548
|
-
if (cost > 0)
|
|
399
|
+
if (cost > 0) {
|
|
549
400
|
reportMcpOutage(OutageStage.RECOVERED, { reason: lastDropReason, attempts: cost });
|
|
401
|
+
}
|
|
550
402
|
attempts = 0;
|
|
551
403
|
attemptsBeforeDormant = 0;
|
|
552
404
|
// The disconnects behind us were reconnects, not a runaway. Counting them for the life of
|
|
@@ -555,11 +407,13 @@ onUsableSession = () => undefined) {
|
|
|
555
407
|
onUsableSession();
|
|
556
408
|
// The new session's McpServer has never seen the client's initialize — replay it first, then
|
|
557
409
|
// flush whatever the client sent while we were reconnecting.
|
|
558
|
-
for (const line of replay.replayLines())
|
|
410
|
+
for (const line of replay.replayLines()) {
|
|
559
411
|
forward(url, line);
|
|
412
|
+
}
|
|
560
413
|
clearQueueTimer();
|
|
561
|
-
for (const queued of stdinQueue.splice(0))
|
|
414
|
+
for (const queued of stdinQueue.splice(0)) {
|
|
562
415
|
forward(url, queued);
|
|
416
|
+
}
|
|
563
417
|
// The client may be holding a tool list we invented. See toolsChangedNotification.
|
|
564
418
|
const relist = toolsChangedNotification(toolsAnnouncedLocally);
|
|
565
419
|
if (relist !== null) {
|
|
@@ -587,10 +441,12 @@ onUsableSession = () => undefined) {
|
|
|
587
441
|
emit(data);
|
|
588
442
|
}
|
|
589
443
|
}
|
|
590
|
-
function scheduleReconnect(rawReason, detail) {
|
|
591
|
-
if (stopped)
|
|
444
|
+
function scheduleReconnect(rawReason, detail, cause) {
|
|
445
|
+
if (stopped) {
|
|
592
446
|
return;
|
|
447
|
+
}
|
|
593
448
|
postUrl = null;
|
|
449
|
+
noteProxyDrop();
|
|
594
450
|
// A stream the daemon warned us about is a planned shutdown, not a fault. The socket looks
|
|
595
451
|
// identical either way from here, so this flag is the only thing that can tell them apart —
|
|
596
452
|
// and without it every scheduled retirement was counted as an outage the agent suffered.
|
|
@@ -610,18 +466,19 @@ onUsableSession = () => undefined) {
|
|
|
610
466
|
// what the old one owed and silence here is permanent.
|
|
611
467
|
stdinQueue.splice(0);
|
|
612
468
|
const lost = streamLossReplies(pending, reason);
|
|
613
|
-
for (const reply of lost)
|
|
469
|
+
for (const reply of lost) {
|
|
614
470
|
emit(reply);
|
|
471
|
+
}
|
|
615
472
|
// How many calls this drop actually killed. Zero means the agent could not feel it.
|
|
616
473
|
const pendingLost = lost.length;
|
|
617
474
|
// Once per process: this session has now lost its tools at least once, which is the number
|
|
618
475
|
// that says whether the transport work actually landed for real users. `pendingLost` is the
|
|
619
476
|
// part an agent can FEEL — almost every measured drop killed nothing in flight.
|
|
620
|
-
reportMcpOutage(OutageStage.FIRST, { reason, attempts, pendingLost });
|
|
477
|
+
reportMcpOutage(OutageStage.FIRST, { reason, attempts, pendingLost, ...cause });
|
|
621
478
|
// The severe stage: we are about to stop retrying. Reported so the share of sessions where
|
|
622
479
|
// MCP never came back on its own is a number rather than an anecdote.
|
|
623
480
|
if (attempts > MAX_RECONNECT_ATTEMPTS) {
|
|
624
|
-
reportMcpOutage(OutageStage.BUDGET_SPENT, { reason, attempts, pendingLost });
|
|
481
|
+
reportMcpOutage(OutageStage.BUDGET_SPENT, { reason, attempts, pendingLost, ...cause });
|
|
625
482
|
// Stop RETRYING, never stop SERVING. See onReconnectBudgetSpent: exiting here made the
|
|
626
483
|
// client mark the MCP server disconnected and left a human to reconnect it by hand.
|
|
627
484
|
dormant = onReconnectBudgetSpent() === OnDrop.DORMANT;
|
|
@@ -669,7 +526,7 @@ onUsableSession = () => undefined) {
|
|
|
669
526
|
function connect(first) {
|
|
670
527
|
// Announce this process to the daemon on the connect we already make — it is the single judge
|
|
671
528
|
// of version skew, and this is the only moment it learns the agent's MCP server exists.
|
|
672
|
-
const announce = `${MCP_SSE_PATH}?${PEER_VERSION_PARAM}=${encodeURIComponent(SERVER_VERSION)}&${PEER_CONTRACT_PARAM}=${encodeURIComponent(CONTRACT_FINGERPRINT)}`;
|
|
529
|
+
const announce = `${MCP_SSE_PATH}?${PEER_VERSION_PARAM}=${encodeURIComponent(SERVER_VERSION)}&${PEER_CONTRACT_PARAM}=${encodeURIComponent(CONTRACT_FINGERPRINT)}&${PROXY_ID_PARAM}=${PROXY_ID}${agentSessionQuery(process.env)}`;
|
|
673
530
|
/**
|
|
674
531
|
* ONE reconnect per connect attempt, whoever notices the failure first.
|
|
675
532
|
*
|
|
@@ -698,24 +555,27 @@ onUsableSession = () => undefined) {
|
|
|
698
555
|
let responded = false;
|
|
699
556
|
const req = http.get({ host: LOOPBACK_HOST, port, path: announce }, (res) => {
|
|
700
557
|
responded = true;
|
|
701
|
-
if (!first)
|
|
558
|
+
if (!first) {
|
|
702
559
|
proxyLog('reticle_mcp_proxy_reconnected', { port });
|
|
560
|
+
}
|
|
703
561
|
res.setEncoding('utf8');
|
|
704
|
-
const drop = (reason, detail) => {
|
|
705
|
-
if (settled)
|
|
562
|
+
const drop = (reason, detail, cause) => {
|
|
563
|
+
if (settled) {
|
|
706
564
|
return;
|
|
565
|
+
}
|
|
707
566
|
settled = true;
|
|
708
567
|
// What the dead session owed is settled inside scheduleReconnect, so the other half of the
|
|
709
568
|
// socket's death pays exactly the same debt when it is the one that notices first.
|
|
710
|
-
scheduleReconnect(reason, detail);
|
|
569
|
+
scheduleReconnect(reason, detail, cause ?? { responded });
|
|
711
570
|
};
|
|
712
571
|
const sse = new SseFrameParser();
|
|
713
572
|
res.on('data', (chunk) => {
|
|
714
|
-
for (const frame of sse.push(chunk))
|
|
573
|
+
for (const frame of sse.push(chunk)) {
|
|
715
574
|
onSseEvent(frame.event, frame.data, port);
|
|
575
|
+
}
|
|
716
576
|
});
|
|
717
577
|
res.on('end', () => drop('sse_ended'));
|
|
718
|
-
res.on('error', (err) => drop('sse_error', err.message));
|
|
578
|
+
res.on('error', (err) => drop('sse_error', err.message, { error: err, responded }));
|
|
719
579
|
// A socket dying under us (daemon killed, network stack reset) emits NEITHER `end` nor
|
|
720
580
|
// `error` — only `aborted`/`close`. Listening for just the first two is why the proxy sat
|
|
721
581
|
// there afterwards holding a dead stream. `settled` keeps the clean case single-fire.
|
|
@@ -725,9 +585,14 @@ onUsableSession = () => undefined) {
|
|
|
725
585
|
// The very first connect failing means there is no daemon to talk to — that is a startup
|
|
726
586
|
// failure the caller handles. Later ones are just the daemon bouncing: keep retrying.
|
|
727
587
|
req.on('error', (err) => {
|
|
728
|
-
if (settled)
|
|
588
|
+
if (settled) {
|
|
729
589
|
return;
|
|
590
|
+
}
|
|
730
591
|
settled = true;
|
|
592
|
+
if (first && !responded && boot !== undefined) {
|
|
593
|
+
bootFailed(); // The start is done, so nothing is still booting on this port.
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
731
596
|
if (first && !responded) {
|
|
732
597
|
// A refused port on the very first connect is a daemon still booting, not a failure. Keep
|
|
733
598
|
// chasing it: nothing else will, because only a fresh client request re-probes the port
|
|
@@ -747,10 +612,57 @@ onUsableSession = () => undefined) {
|
|
|
747
612
|
reject(err);
|
|
748
613
|
return;
|
|
749
614
|
}
|
|
750
|
-
|
|
615
|
+
// A reset of a stream that was already serving also lands here, because the request sees
|
|
616
|
+
// the error before the response closes. That is a dropped stream, not a refused connect.
|
|
617
|
+
const reason = responded ? OutageReason.SSE_RESET : OutageReason.CONNECT_ERROR;
|
|
618
|
+
scheduleReconnect(reason, err.message, { error: err, responded });
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
/** The ONLY path that starts a daemon (see proxy-lifecycle.ts). `endpoint` flushes the queue. */
|
|
622
|
+
function wake() {
|
|
623
|
+
dormant = false;
|
|
624
|
+
// Budget reset, accounting preserved. See attemptsBeforeDormant.
|
|
625
|
+
attemptsBeforeDormant += attempts;
|
|
626
|
+
attempts = 0;
|
|
627
|
+
void (ensureDaemon?.() ?? Promise.resolve())
|
|
628
|
+
.then((resolved) => {
|
|
629
|
+
port = resolved ?? port;
|
|
630
|
+
return connect(false);
|
|
631
|
+
})
|
|
632
|
+
.catch((err) => {
|
|
633
|
+
dormant = true;
|
|
634
|
+
const reason = withStateDirDiagnostic(err instanceof Error ? err.message : String(err));
|
|
635
|
+
proxyLog('reticle_mcp_proxy_wake_failed', { port, error: reason });
|
|
636
|
+
// The failure IS the answer to everything the wake was for. Do not wait for the timer.
|
|
637
|
+
expireQueue(reason);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
if (boot === undefined) {
|
|
641
|
+
connect(true);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
// Dial when the start is done, at the port it settled on. A failed start dials too, because
|
|
645
|
+
// only the dial can tell the two cases apart. A refused dial means no daemon: see bootFailed.
|
|
646
|
+
// A process that accepts and then drops the stream is a daemon that flaps, and the normal
|
|
647
|
+
// retry loop backs off from it and logs each retry.
|
|
648
|
+
void boot.ready
|
|
649
|
+
.catch(() => undefined)
|
|
650
|
+
.then((resolved) => {
|
|
651
|
+
port = resolved ?? port;
|
|
652
|
+
connect(true);
|
|
751
653
|
});
|
|
752
654
|
}
|
|
753
|
-
|
|
655
|
+
/**
|
|
656
|
+
* The first dial after the start was refused, so no daemon is there. For example, it died
|
|
657
|
+
* before or just after it served. The first-connect retry never starts a daemon, so a queued
|
|
658
|
+
* call waited until the queue expired. Go dormant, and wake now for a request that waits.
|
|
659
|
+
*/
|
|
660
|
+
function bootFailed() {
|
|
661
|
+
dormant = true;
|
|
662
|
+
if (0 < stdinQueue.length) {
|
|
663
|
+
wake();
|
|
664
|
+
}
|
|
665
|
+
}
|
|
754
666
|
// ── stdin reader ─────────────────────────────────────────────────────────
|
|
755
667
|
process.stdin.setEncoding('utf8');
|
|
756
668
|
let handshakeAnswered = false;
|
|
@@ -770,13 +682,15 @@ onUsableSession = () => undefined) {
|
|
|
770
682
|
* and `handshakeAnswered` keeps us from answering twice.
|
|
771
683
|
*/
|
|
772
684
|
const armLocalHandshake = (line) => {
|
|
773
|
-
if (handshakeAnswered)
|
|
685
|
+
if (handshakeAnswered) {
|
|
774
686
|
return;
|
|
687
|
+
}
|
|
775
688
|
// Only an `initialize` REQUEST is ours to answer. Asked with no instructions because the ones
|
|
776
689
|
// we will actually send name the reason the daemon failed, and the reason is not known until
|
|
777
690
|
// the deliver closure runs — see degradedInstructions.
|
|
778
|
-
if (null === localInitializeResponse(line, ''))
|
|
691
|
+
if (null === localInitializeResponse(line, '')) {
|
|
779
692
|
return;
|
|
693
|
+
}
|
|
780
694
|
// Take the debt NOW, not when the timer fires.
|
|
781
695
|
//
|
|
782
696
|
// Three paths can answer this id — the daemon, the stream-loss drain, and the timer below —
|
|
@@ -789,18 +703,27 @@ onUsableSession = () => undefined) {
|
|
|
789
703
|
// established first, the `endpoint` handler sets `handshakeAnswered` before the queue is
|
|
790
704
|
// flushed, which disarms the timer and leaves the daemon's own reply the only one.
|
|
791
705
|
const claimed = parseJsonRpc(line);
|
|
792
|
-
if (claimed?.id !== undefined)
|
|
706
|
+
if (claimed?.id !== undefined) {
|
|
793
707
|
pending.take(claimed.id);
|
|
794
|
-
|
|
795
|
-
|
|
708
|
+
}
|
|
709
|
+
deliverHandshakeLocally = (reason, tools) => {
|
|
710
|
+
if (handshakeAnswered || postUrl !== null) {
|
|
796
711
|
return;
|
|
712
|
+
}
|
|
797
713
|
// Built here, not at arm time, so the notice can name WHY no daemon answered. The client
|
|
798
714
|
// reads `instructions` exactly once, at initialize, and this response is the only one it
|
|
799
715
|
// will ever get — so a handshake that does not say it is unbacked never says it at all.
|
|
800
|
-
// See degradedInstructions for the field report this closes.
|
|
801
|
-
|
|
802
|
-
|
|
716
|
+
// See degradedInstructions for the field report this closes. With `tools`, the daemon is
|
|
717
|
+
// starting: the instructions are the healthy ones, and the list is the daemon's own.
|
|
718
|
+
const response = localInitializeResponse(line, tools === undefined
|
|
719
|
+
? degradedInstructions(proxyInstructions(port), port, withStateDirDiagnostic(reason))
|
|
720
|
+
: proxyInstructions(port, tools.map((tool) => tool.name)));
|
|
721
|
+
if (null === response) {
|
|
803
722
|
return;
|
|
723
|
+
}
|
|
724
|
+
if (tools !== undefined) {
|
|
725
|
+
catalog.remember(tools);
|
|
726
|
+
}
|
|
804
727
|
handshakeAnswered = true;
|
|
805
728
|
deliverHandshakeLocally = undefined;
|
|
806
729
|
// The catalog we are about to serve is ours, not the daemon's, so it has to be corrected
|
|
@@ -812,19 +735,35 @@ onUsableSession = () => undefined) {
|
|
|
812
735
|
// handshake: `replayLines` re-issues it under a distinct id and suppresses the echo.
|
|
813
736
|
for (let i = stdinQueue.length - 1; i >= 0; i--) {
|
|
814
737
|
const queued = stdinQueue[i];
|
|
815
|
-
if (queued !== undefined && isHandshakeLine(queued))
|
|
738
|
+
if (queued !== undefined && isHandshakeLine(queued)) {
|
|
816
739
|
stdinQueue.splice(i, 1);
|
|
740
|
+
}
|
|
817
741
|
}
|
|
818
742
|
proxyLog('reticle_mcp_local_handshake', { port, reason });
|
|
819
743
|
emit(response);
|
|
744
|
+
// A `tools/list` that came before this answer is in the queue. Answer it from the catalog.
|
|
745
|
+
for (let i = stdinQueue.length - 1; i >= 0; i--) {
|
|
746
|
+
const cached = catalog.answer(stdinQueue[i] ?? '');
|
|
747
|
+
if (cached !== null) {
|
|
748
|
+
stdinQueue.splice(i, 1);
|
|
749
|
+
emit(cached);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
820
752
|
};
|
|
821
753
|
setTimeout(() => deliverHandshakeLocally?.('daemon did not answer in time'), LOCAL_HANDSHAKE_MS).unref();
|
|
754
|
+
// Our own daemon is starting on a free port. Answer now: some clients start their turn before
|
|
755
|
+
// a daemon can be up, and they do not wait for MCP servers.
|
|
756
|
+
void boot?.spawning
|
|
757
|
+
.then(boot.tools)
|
|
758
|
+
.then((tools) => deliverHandshakeLocally?.('the daemon is starting', tools))
|
|
759
|
+
.catch(() => undefined);
|
|
822
760
|
};
|
|
823
761
|
let stdinBuffer = '';
|
|
824
762
|
let stdinDiscarding = false;
|
|
825
763
|
process.stdin.on('data', (chunk) => {
|
|
826
|
-
if (stopped)
|
|
764
|
+
if (stopped) {
|
|
827
765
|
return;
|
|
766
|
+
}
|
|
828
767
|
idleExit.noteTraffic();
|
|
829
768
|
// Not `(buffer + chunk).split()` inline — that rescans everything held on every chunk, so one
|
|
830
769
|
// large line cost O(n²) in its own size and pinned the event loop for tens of seconds, during
|
|
@@ -841,21 +780,25 @@ onUsableSession = () => undefined) {
|
|
|
841
780
|
}
|
|
842
781
|
for (const line of drained.lines) {
|
|
843
782
|
const trimmed = line.trim();
|
|
844
|
-
if ('' === trimmed)
|
|
783
|
+
if ('' === trimmed) {
|
|
845
784
|
continue;
|
|
785
|
+
}
|
|
846
786
|
replay.observeOutbound(trimmed);
|
|
847
787
|
pending.observeOutbound(trimmed);
|
|
848
788
|
// The ARRIVAL of the request, not the delivery of an answer: "this client asked for the tool
|
|
849
789
|
// list" is the fact `status` reports on, and a request that arrived and then failed is a
|
|
850
790
|
// different problem with its own diagnosis. Recording the answer instead would let a host
|
|
851
791
|
// that never asks look identical to one that asked and was refused.
|
|
852
|
-
if (isToolsListRequest(trimmed))
|
|
792
|
+
if (isToolsListRequest(trimmed)) {
|
|
853
793
|
rememberEnumerated(reticleStateHome(), port);
|
|
794
|
+
}
|
|
854
795
|
// The hop every other check misses. Recorded on the REQUEST rather than a reply, because
|
|
855
796
|
// the question is whether anything ever left the agent — a call that leaves and gets no
|
|
856
797
|
// answer is a different fault from one that was never made, and only this side sees it.
|
|
857
|
-
if (isToolCallRequest(trimmed))
|
|
798
|
+
if (isToolCallRequest(trimmed)) {
|
|
858
799
|
rememberToolCalled(reticleStateHome(), port);
|
|
800
|
+
noteProxyToolCall();
|
|
801
|
+
}
|
|
859
802
|
const action = onClientRequest(postUrl !== null, dormant);
|
|
860
803
|
if (action === OnRequest.SEND && postUrl !== null) {
|
|
861
804
|
forward(postUrl, trimmed);
|
|
@@ -878,26 +821,8 @@ onUsableSession = () => undefined) {
|
|
|
878
821
|
// Answer it ourselves after a bounded wait; the daemon still receives the client's own
|
|
879
822
|
// initialize when a session is finally established (replayLines).
|
|
880
823
|
armLocalHandshake(trimmed);
|
|
881
|
-
// WAKE is the ONLY path allowed to start a daemon — see proxy-lifecycle.ts. The queue is
|
|
882
|
-
// flushed once the new session's `endpoint` frame arrives.
|
|
883
824
|
if (action === OnRequest.WAKE) {
|
|
884
|
-
|
|
885
|
-
// Budget reset, accounting preserved. See attemptsBeforeDormant.
|
|
886
|
-
attemptsBeforeDormant += attempts;
|
|
887
|
-
attempts = 0;
|
|
888
|
-
void (ensureDaemon?.() ?? Promise.resolve())
|
|
889
|
-
.then(() => connect(false))
|
|
890
|
-
.catch((err) => {
|
|
891
|
-
dormant = true;
|
|
892
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
893
|
-
proxyLog('reticle_mcp_proxy_wake_failed', { port, error: reason });
|
|
894
|
-
// The wake is the only thing that can start a daemon, so its failure IS the answer to
|
|
895
|
-
// everything it was woken for. It used to be logged and nothing else: the request sat
|
|
896
|
-
// out the full QUEUE_WAIT_MS and was then refused by a timer that could only say "no
|
|
897
|
-
// daemon could be reached" — twenty seconds later, and less true, than the sentence
|
|
898
|
-
// already in hand.
|
|
899
|
-
expireQueue(reason);
|
|
900
|
-
});
|
|
825
|
+
wake();
|
|
901
826
|
}
|
|
902
827
|
}
|
|
903
828
|
});
|
|
@@ -909,10 +834,11 @@ onUsableSession = () => undefined) {
|
|
|
909
834
|
// between the writes made it "work", which is the signature of a teardown race, not of a client
|
|
910
835
|
// that asked for too much. Drain first, and let the exit status carry the truth if it fails.
|
|
911
836
|
process.stdin.on('end', () => {
|
|
912
|
-
if (stopped)
|
|
837
|
+
if (stopped) {
|
|
913
838
|
return;
|
|
839
|
+
}
|
|
914
840
|
if (0 === pending.unanswered.length) {
|
|
915
|
-
quit(0);
|
|
841
|
+
quit(0, ProxyExitReason.STDIN_END);
|
|
916
842
|
return;
|
|
917
843
|
}
|
|
918
844
|
const deadline = Date.now() + SHUTDOWN_DRAIN_MS;
|
|
@@ -920,14 +846,15 @@ onUsableSession = () => undefined) {
|
|
|
920
846
|
const stuck = pending.unanswered;
|
|
921
847
|
if (0 === stuck.length) {
|
|
922
848
|
clearInterval(poll);
|
|
923
|
-
quit(0);
|
|
849
|
+
quit(0, ProxyExitReason.STDIN_END);
|
|
924
850
|
return;
|
|
925
851
|
}
|
|
926
|
-
if (Date.now() < deadline)
|
|
852
|
+
if (Date.now() < deadline) {
|
|
927
853
|
return;
|
|
854
|
+
}
|
|
928
855
|
clearInterval(poll);
|
|
929
856
|
proxyLog('reticle_mcp_proxy_unanswered_at_exit', { port, ids: stuck });
|
|
930
|
-
quit(1);
|
|
857
|
+
quit(1, ProxyExitReason.STDIN_END);
|
|
931
858
|
}, SHUTDOWN_DRAIN_POLL_MS);
|
|
932
859
|
});
|
|
933
860
|
});
|