@reticlehq/server 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +93 -111
- package/dist/bridge/auth-failure-reason.d.ts +6 -2
- package/dist/bridge/auth-failure-reason.js +15 -8
- package/dist/bridge/bridge.d.ts +18 -14
- package/dist/bridge/bridge.js +248 -165
- package/dist/bridge/bridge.test-harness.d.ts +2 -0
- package/dist/bridge/bridge.test-harness.js +8 -1
- package/dist/bridge/pairing-token.d.ts +5 -0
- package/dist/bridge/pairing-token.js +28 -0
- package/dist/bridge/protocol-skew.d.ts +37 -0
- package/dist/bridge/protocol-skew.js +45 -0
- package/dist/capsule/capsule-store.d.ts +8 -7
- package/dist/capsule/capsule-store.js +1 -1
- package/dist/capsule/causal-summary.d.ts +2 -2
- package/dist/capsule/causal-summary.js +7 -2
- package/dist/chromium-launch-options.d.ts +28 -0
- package/dist/chromium-launch-options.js +29 -0
- package/dist/cli/cli-doctor.js +39 -1
- package/dist/cli/cli-flow-commands.d.ts +3 -1
- package/dist/cli/cli-flow-commands.js +34 -4
- package/dist/cli/cli-kill.d.ts +2 -1
- package/dist/cli/cli-launch.d.ts +11 -1
- package/dist/cli/cli-launch.js +59 -10
- package/dist/cli/cli-parse.d.ts +17 -1
- package/dist/cli/cli-parse.js +238 -12
- package/dist/cli/cli-update-commands.js +20 -1
- package/dist/cli/cli-verify.d.ts +2 -1
- package/dist/cli/cli-verify.js +5 -4
- package/dist/cli/cloud-cli.js +428 -253
- package/dist/cli/cloud-keystore.d.ts +37 -0
- package/dist/cli/cloud-keystore.js +97 -0
- package/dist/cli/cloud-kit.d.ts +37 -0
- package/dist/cli/cloud-kit.js +121 -0
- package/dist/cli/cloud-login.d.ts +17 -0
- package/dist/cli/cloud-login.js +183 -0
- package/dist/cli/cloud-session.d.ts +48 -0
- package/dist/cli/cloud-session.js +53 -0
- package/dist/cli/config-discovery.d.ts +2 -1
- package/dist/cli/config-discovery.js +1 -0
- package/dist/cli/daemon-start-options.d.ts +32 -0
- package/dist/cli/daemon-start-options.js +38 -0
- package/dist/cli/device-flow.d.ts +76 -0
- package/dist/cli/device-flow.js +56 -0
- package/dist/cli/doctor-rows.d.ts +12 -1
- package/dist/cli/doctor-rows.js +12 -1
- package/dist/cli/drive-attach.d.ts +13 -0
- package/dist/cli/drive-attach.js +18 -0
- package/dist/cli/drive-command.js +7 -3
- package/dist/cli/gate-exit.d.ts +21 -0
- package/dist/cli/gate-exit.js +21 -0
- package/dist/cli/gate-hook-message.d.ts +33 -0
- package/dist/cli/gate-hook-message.js +46 -0
- package/dist/cli/mcp-command.d.ts +25 -0
- package/dist/cli/mcp-command.js +174 -0
- package/dist/cli/project-name.d.ts +30 -0
- package/dist/cli/project-name.js +44 -0
- package/dist/cli/sibling-ports.d.ts +37 -0
- package/dist/cli/sibling-ports.js +56 -0
- package/dist/cli/status-next-action.d.ts +14 -0
- package/dist/cli/status-next-action.js +4 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +167 -186
- package/dist/cloud/cloud-config.d.ts +24 -2
- package/dist/cloud/cloud-config.js +64 -5
- package/dist/cloud/cloud-sync.d.ts +16 -5
- package/dist/cloud/cloud-sync.js +64 -1
- package/dist/cloud/sync-cycle.d.ts +110 -0
- package/dist/cloud/sync-cycle.js +270 -0
- package/dist/cloud/sync-daemon.d.ts +69 -0
- package/dist/cloud/sync-daemon.js +260 -0
- package/dist/cloud/sync-disk.d.ts +9 -0
- package/dist/cloud/sync-disk.js +191 -0
- package/dist/cloud/sync-hash.d.ts +2 -0
- package/dist/cloud/sync-hash.js +35 -0
- package/dist/crawl/crawl-tools.js +5 -3
- package/dist/crawl/crawl.d.ts +12 -13
- package/dist/crawl/crawl.js +25 -3
- package/dist/daemon/daemon-resolve.d.ts +122 -0
- package/dist/daemon/daemon-resolve.js +187 -0
- package/dist/daemon/dev-servers.d.ts +15 -0
- package/dist/daemon/dev-servers.js +68 -0
- package/dist/daemon/free-port.d.ts +29 -0
- package/dist/daemon/free-port.js +55 -0
- package/dist/daemon/heartbeat.d.ts +0 -4
- package/dist/daemon/heartbeat.js +2 -2
- package/dist/daemon/wake-decision.d.ts +32 -0
- package/dist/daemon/wake-decision.js +38 -0
- package/dist/dev/stale-issue-guard.d.ts +0 -2
- package/dist/dev/stale-issue-guard.js +1 -1
- package/dist/domain/domain-tools.js +2 -2
- package/dist/events/contradictions.d.ts +59 -0
- package/dist/events/contradictions.js +253 -27
- package/dist/events/declared.d.ts +17 -0
- package/dist/events/declared.js +43 -1
- package/dist/events/echo-mismatch.js +65 -1
- package/dist/events/event-filters.d.ts +8 -0
- package/dist/events/event-filters.js +19 -2
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +17 -0
- package/dist/events/predicate-asks.d.ts +13 -0
- package/dist/events/predicate-asks.js +22 -0
- package/dist/events/predicate-eval.js +117 -27
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +43 -3
- package/dist/events/predicate-precheck.d.ts +8 -0
- package/dist/events/predicate-precheck.js +41 -0
- package/dist/events/predicate-route.d.ts +14 -0
- package/dist/events/predicate-route.js +36 -8
- package/dist/events/predicate-schema.d.ts +44 -0
- package/dist/events/predicate-schema.js +126 -23
- package/dist/events/predicate.d.ts +20 -0
- package/dist/events/predicate.js +77 -6
- package/dist/events/split-text-miss.d.ts +30 -0
- package/dist/events/split-text-miss.js +44 -0
- package/dist/flows/coverage.d.ts +0 -8
- package/dist/flows/coverage.js +1 -1
- package/dist/flows/decision.d.ts +5 -1
- package/dist/flows/decision.js +33 -7
- package/dist/flows/flake.d.ts +6 -8
- package/dist/flows/flake.js +1 -1
- package/dist/flows/flow-intent.d.ts +52 -0
- package/dist/flows/flow-intent.js +123 -0
- package/dist/flows/flow-memory-consult.d.ts +55 -0
- package/dist/flows/flow-memory-consult.js +38 -0
- package/dist/flows/flow-replay-run.d.ts +18 -7
- package/dist/flows/flow-replay-run.js +263 -23
- package/dist/flows/flow-replay.js +11 -2
- package/dist/flows/flow-step-runners.d.ts +8 -0
- package/dist/flows/flow-step-runners.js +8 -2
- package/dist/flows/flow-store-for-session.d.ts +31 -0
- package/dist/flows/flow-store-for-session.js +33 -0
- package/dist/flows/flow-success.js +15 -3
- package/dist/flows/flow-tools.js +87 -33
- package/dist/flows/flows.d.ts +14 -1
- package/dist/flows/flows.js +502 -24
- package/dist/flows/heal-run.js +5 -2
- package/dist/flows/predicate-to-expect.d.ts +17 -12
- package/dist/flows/predicate-to-expect.js +36 -15
- package/dist/flows/recordings.d.ts +13 -1
- package/dist/flows/recordings.js +17 -2
- package/dist/flows/replay.d.ts +22 -2
- package/dist/flows/replay.js +28 -2
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +9 -3
- package/dist/honesty/blind-spots.d.ts +51 -15
- package/dist/honesty/blind-spots.js +64 -4
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/honesty.d.ts +24 -0
- package/dist/honesty/honesty.js +25 -1
- package/dist/honesty/instrumentation-gaps.d.ts +39 -6
- package/dist/honesty/instrumentation-gaps.js +88 -5
- package/dist/honesty/tool-hit-rate.d.ts +86 -0
- package/dist/honesty/tool-hit-rate.js +90 -0
- package/dist/honesty/undeclared-change.d.ts +51 -0
- package/dist/honesty/undeclared-change.js +56 -0
- package/dist/honesty/verified.d.ts +16 -0
- package/dist/honesty/verified.js +84 -5
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +13 -1
- package/dist/hunt/hunt-report.d.ts +0 -2
- package/dist/hunt/hunt-report.js +1 -1
- package/dist/impact/impact-recorder.d.ts +31 -8
- package/dist/impact/impact-recorder.js +104 -21
- package/dist/impact/impact-store.d.ts +13 -4
- package/dist/impact/impact-store.js +44 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.js +113 -7
- package/dist/init/agent-rules.d.ts +30 -6
- package/dist/init/agent-rules.js +54 -13
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +28 -0
- package/dist/init/capabilities.d.ts +4 -3
- package/dist/init/capabilities.js +23 -3
- package/dist/init/closing-hint.d.ts +36 -0
- package/dist/init/closing-hint.js +120 -0
- package/dist/init/confirm.d.ts +27 -28
- package/dist/init/confirm.js +51 -11
- package/dist/init/cra.d.ts +34 -5
- package/dist/init/cra.js +96 -25
- package/dist/init/csp-check.js +17 -2
- package/dist/init/csp-doctor.d.ts +8 -1
- package/dist/init/csp-doctor.js +8 -3
- package/dist/init/dev-script.d.ts +50 -0
- package/dist/init/dev-script.js +66 -0
- package/dist/init/format-generated.d.ts +18 -0
- package/dist/init/format-generated.js +39 -0
- package/dist/init/install-hint.d.ts +9 -0
- package/dist/init/install-hint.js +29 -0
- package/dist/init/mcp-clients.d.ts +7 -0
- package/dist/init/mcp-clients.js +7 -0
- package/dist/init/mcp.d.ts +17 -6
- package/dist/init/mcp.js +27 -11
- package/dist/init/node-io.js +11 -1
- package/dist/init/plan-framework.d.ts +0 -5
- package/dist/init/plan-framework.js +147 -37
- package/dist/init/plan.d.ts +30 -37
- package/dist/init/plan.js +50 -60
- package/dist/init/preflight.d.ts +28 -0
- package/dist/init/preflight.js +38 -0
- package/dist/init/refresh-rules.d.ts +39 -0
- package/dist/init/refresh-rules.js +51 -0
- package/dist/init/run.d.ts +32 -35
- package/dist/init/run.js +151 -202
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/snippets.js +90 -31
- package/dist/init/vite-config.d.ts +1 -1
- package/dist/init/vite-config.js +39 -25
- package/dist/init/workspace-apps.d.ts +17 -10
- package/dist/init/workspace-apps.js +110 -0
- package/dist/input/network-mock-tools.js +3 -6
- package/dist/input/real-input.d.ts +2 -0
- package/dist/input/real-input.js +28 -7
- package/dist/input/scroll-find.d.ts +7 -4
- package/dist/input/scroll-find.js +63 -25
- package/dist/input/scroll-tools.js +4 -6
- package/dist/input/viewport-tools.js +5 -4
- package/dist/intent/inline-intent.d.ts +37 -0
- package/dist/intent/inline-intent.js +193 -0
- package/dist/intent/intent-shard-store.d.ts +72 -0
- package/dist/intent/intent-shard-store.js +203 -0
- package/dist/intent/intent-shard.d.ts +411 -0
- package/dist/intent/intent-shard.js +176 -0
- package/dist/intent/intent-store.d.ts +49 -0
- package/dist/intent/intent-store.js +133 -0
- package/dist/intent/intent-subject.d.ts +45 -0
- package/dist/intent/intent-subject.js +177 -0
- package/dist/intent/intent-tools.d.ts +2 -0
- package/dist/intent/intent-tools.js +143 -0
- package/dist/intent/open-intents.d.ts +42 -0
- package/dist/intent/open-intents.js +40 -0
- package/dist/journal/envelope.d.ts +26 -26
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/license/license-env.d.ts +29 -0
- package/dist/license/license-env.js +148 -0
- package/dist/loopback-agent.d.ts +31 -0
- package/dist/loopback-agent.js +57 -0
- package/dist/mcp/attach-memory.d.ts +83 -0
- package/dist/mcp/attach-memory.js +185 -0
- package/dist/mcp/client-identity.d.ts +31 -0
- package/dist/mcp/client-identity.js +51 -0
- package/dist/mcp/mcp-post-transport.d.ts +26 -0
- package/dist/mcp/mcp-post-transport.js +109 -0
- package/dist/mcp/mcp-proxy.d.ts +2 -8
- package/dist/mcp/mcp-proxy.js +64 -62
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +52 -24
- package/dist/mcp/proxy-backoff.d.ts +11 -0
- package/dist/mcp/proxy-backoff.js +13 -0
- package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
- package/dist/mcp/proxy-daemon-probe.js +7 -1
- package/dist/mcp/proxy-handshake.d.ts +23 -0
- package/dist/mcp/proxy-handshake.js +31 -0
- package/dist/mcp/server-instructions.js +45 -3
- package/dist/mcp/tool-catalog-cache.d.ts +16 -0
- package/dist/mcp/tool-catalog-cache.js +22 -0
- package/dist/memory/memory-tools.d.ts +2 -0
- package/dist/memory/memory-tools.js +82 -0
- package/dist/memory/project-memory.d.ts +34 -0
- package/dist/memory/project-memory.js +96 -0
- package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
- package/dist/oracles/flow-instrument-gaps.js +4 -5
- package/dist/oracles/propose-consequences.js +12 -3
- package/dist/oracles/self-instrument.d.ts +9 -9
- package/dist/oracles/self-instrument.js +19 -13
- package/dist/pool/browser-pool.d.ts +75 -5
- package/dist/pool/browser-pool.js +144 -5
- package/dist/pool/playwright-launcher.js +12 -1
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.js +6 -1
- package/dist/project/project-tools.js +6 -35
- package/dist/project/reticle-dir.d.ts +17 -2
- package/dist/project/reticle-dir.js +22 -4
- package/dist/runs/context-tools.d.ts +2 -0
- package/dist/runs/context-tools.js +89 -0
- package/dist/runs/run-context.d.ts +40 -0
- package/dist/runs/run-context.js +158 -0
- package/dist/runs/run-diff.d.ts +2 -1
- package/dist/runs/run-store.d.ts +10 -0
- package/dist/runs/run-store.js +10 -0
- package/dist/runs/verification-sync.d.ts +0 -11
- package/dist/runs/verification-sync.js +24 -3
- package/dist/runs/verify-server.d.ts +8 -1
- package/dist/runs/verify-server.js +14 -2
- package/dist/session/command-timeout.d.ts +5 -2
- package/dist/session/command-timeout.js +5 -4
- package/dist/session/dev-server-probe.d.ts +0 -8
- package/dist/session/dev-server-probe.js +1 -1
- package/dist/session/last-act.d.ts +7 -1
- package/dist/session/last-act.js +2 -2
- package/dist/session/lease-visibility.d.ts +67 -0
- package/dist/session/lease-visibility.js +76 -0
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +33 -0
- package/dist/session/no-session-diagnosis.js +82 -24
- package/dist/session/no-session-next-action.d.ts +12 -0
- package/dist/session/no-session-next-action.js +77 -6
- package/dist/session/no-session-watch.d.ts +18 -6
- package/dist/session/no-session-watch.js +125 -75
- package/dist/session/output-budget.js +4 -1
- package/dist/session/pending-commands.d.ts +11 -1
- package/dist/session/pending-commands.js +12 -2
- package/dist/session/resolve-within.d.ts +34 -0
- package/dist/session/resolve-within.js +53 -0
- package/dist/session/session-health.d.ts +9 -0
- package/dist/session/session-health.js +23 -0
- package/dist/session/session-info.d.ts +18 -0
- package/dist/session/session-info.js +11 -0
- package/dist/session/session-manager.d.ts +14 -0
- package/dist/session/session-manager.js +111 -4
- package/dist/session/session-mirror.d.ts +16 -0
- package/dist/session/session-mirror.js +22 -0
- package/dist/session/session-recommendation.d.ts +19 -5
- package/dist/session/session-recommendation.js +25 -9
- package/dist/session/session-successor.d.ts +37 -0
- package/dist/session/session-successor.js +113 -0
- package/dist/session/session-tools.js +2 -2
- package/dist/session/session.d.ts +53 -3
- package/dist/session/session.js +162 -9
- package/dist/session/wall-clock.d.ts +12 -0
- package/dist/session/wall-clock.js +14 -0
- package/dist/setup/agent-configs.d.ts +96 -0
- package/dist/setup/agent-configs.js +333 -0
- package/dist/setup/agent-io.d.ts +9 -0
- package/dist/setup/agent-io.js +16 -0
- package/dist/setup/agent-writer.d.ts +51 -0
- package/dist/setup/agent-writer.js +115 -0
- package/dist/setup/approval-migration.d.ts +47 -0
- package/dist/setup/approval-migration.js +68 -0
- package/dist/setup/auto-approve.d.ts +85 -0
- package/dist/setup/auto-approve.js +150 -0
- package/dist/setup/bridge-port.d.ts +15 -0
- package/dist/setup/bridge-port.js +24 -0
- package/dist/setup/desktop-shape.d.ts +57 -0
- package/dist/setup/desktop-shape.js +63 -0
- package/dist/setup/dev-server-wait.d.ts +60 -0
- package/dist/setup/dev-server-wait.js +65 -0
- package/dist/setup/drive-agent.d.ts +71 -0
- package/dist/setup/drive-agent.js +171 -0
- package/dist/setup/drive-plan.d.ts +68 -0
- package/dist/setup/drive-plan.js +130 -0
- package/dist/setup/ensure-daemon.d.ts +29 -0
- package/dist/setup/ensure-daemon.js +58 -0
- package/dist/setup/init-runtime.d.ts +47 -0
- package/dist/setup/init-runtime.js +160 -0
- package/dist/setup/interrupt.d.ts +25 -0
- package/dist/setup/interrupt.js +40 -0
- package/dist/setup/license-key.d.ts +39 -0
- package/dist/setup/license-key.js +72 -0
- package/dist/setup/listeners.d.ts +37 -0
- package/dist/setup/listeners.js +85 -0
- package/dist/setup/node-effects.d.ts +60 -0
- package/dist/setup/node-effects.js +210 -0
- package/dist/setup/page-probe.d.ts +39 -0
- package/dist/setup/page-probe.js +49 -0
- package/dist/setup/relaunch.d.ts +36 -0
- package/dist/setup/relaunch.js +57 -0
- package/dist/setup/remaining-steps.d.ts +19 -0
- package/dist/setup/remaining-steps.js +31 -0
- package/dist/setup/run-setup.d.ts +99 -0
- package/dist/setup/run-setup.js +187 -0
- package/dist/setup/session-pick.d.ts +47 -0
- package/dist/setup/session-pick.js +51 -0
- package/dist/setup/setup-command.d.ts +45 -0
- package/dist/setup/setup-command.js +199 -0
- package/dist/setup/setup-options.d.ts +74 -0
- package/dist/setup/setup-options.js +67 -0
- package/dist/setup/transcripts.d.ts +16 -0
- package/dist/setup/transcripts.js +43 -0
- package/dist/status-payload.d.ts +14 -1
- package/dist/status-payload.js +23 -1
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/daemon-telemetry.d.ts +8 -1
- package/dist/telemetry/daemon-telemetry.js +52 -5
- package/dist/telemetry/error-class.js +1 -1
- package/dist/telemetry/feedback-context.d.ts +9 -1
- package/dist/telemetry/feedback-context.js +60 -29
- package/dist/telemetry/feedback-local.d.ts +15 -0
- package/dist/telemetry/feedback-local.js +56 -0
- package/dist/telemetry/feedback-outbox.d.ts +2 -1
- package/dist/telemetry/feedback.d.ts +11 -0
- package/dist/telemetry/feedback.js +19 -6
- package/dist/telemetry/init-telemetry.d.ts +2 -0
- package/dist/telemetry/init-telemetry.js +2 -0
- package/dist/telemetry/install-source.d.ts +14 -0
- package/dist/telemetry/install-source.js +30 -0
- package/dist/telemetry/license-activation.d.ts +7 -1
- package/dist/telemetry/license-activation.js +9 -5
- package/dist/telemetry/mcp-connection.js +7 -0
- package/dist/telemetry/project-profile.d.ts +12 -9
- package/dist/telemetry/project-profile.js +12 -7
- package/dist/telemetry/proxy-telemetry.d.ts +3 -0
- package/dist/telemetry/proxy-telemetry.js +21 -0
- package/dist/telemetry/run-telemetry.d.ts +0 -2
- package/dist/telemetry/run-telemetry.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +12 -0
- package/dist/telemetry/session-metrics.js +35 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +3 -1
- package/dist/telemetry/tool-refused.js +34 -2
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-capsule.d.ts +10 -0
- package/dist/tools/act-capsule.js +8 -2
- package/dist/tools/act-danger.d.ts +2 -1
- package/dist/tools/act-danger.js +11 -3
- package/dist/tools/act-observation.d.ts +41 -0
- package/dist/tools/act-observation.js +36 -0
- package/dist/tools/act-preflight.d.ts +20 -0
- package/dist/tools/act-preflight.js +98 -0
- package/dist/tools/act-sequence-retry.d.ts +45 -0
- package/dist/tools/act-sequence-retry.js +58 -0
- package/dist/tools/act-sequence-tool.d.ts +13 -0
- package/dist/tools/act-sequence-tool.js +135 -0
- package/dist/tools/act-target.d.ts +14 -0
- package/dist/tools/act-target.js +47 -0
- package/dist/tools/act-tools.d.ts +21 -1
- package/dist/tools/act-tools.js +196 -204
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +20 -3
- package/dist/tools/assert-verdict.js +58 -20
- package/dist/tools/browser-tools.js +6 -22
- package/dist/tools/complete-snapshot.d.ts +48 -0
- package/dist/tools/complete-snapshot.js +75 -0
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +123 -69
- package/dist/tools/dynamic-tools.js +53 -13
- package/dist/tools/error-recovery.d.ts +25 -14
- package/dist/tools/error-recovery.js +78 -9
- package/dist/tools/feedback-tools.d.ts +1 -1
- package/dist/tools/feedback-tools.js +2 -2
- package/dist/tools/invoke-tool.js +121 -11
- package/dist/tools/lease-hint.d.ts +10 -0
- package/dist/tools/lease-hint.js +53 -4
- package/dist/tools/lease-tools.d.ts +35 -5
- package/dist/tools/lease-tools.js +191 -13
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +22 -0
- package/dist/tools/navigate-arrival.d.ts +0 -2
- package/dist/tools/navigate-arrival.js +1 -1
- package/dist/tools/numeric-bounds.d.ts +100 -0
- package/dist/tools/numeric-bounds.js +125 -0
- package/dist/tools/observe-tools.js +109 -65
- package/dist/tools/read-tools.js +31 -8
- package/dist/tools/real-input-attempt.d.ts +32 -0
- package/dist/tools/real-input-attempt.js +212 -2
- package/dist/tools/reconcile-tools.js +22 -8
- package/dist/tools/resolve-target.d.ts +0 -7
- package/dist/tools/sdk-marker-probe.d.ts +29 -0
- package/dist/tools/sdk-marker-probe.js +57 -0
- package/dist/tools/snapshot-delta.d.ts +1 -1
- package/dist/tools/tool-kit.d.ts +24 -0
- package/dist/tools/tool-kit.js +21 -0
- package/dist/tools/tool-names.d.ts +5 -0
- package/dist/tools/tool-names.js +5 -0
- package/dist/tools/tool-surface.d.ts +85 -6
- package/dist/tools/tool-surface.js +122 -2
- package/dist/tools/tools-helpers.d.ts +19 -0
- package/dist/tools/tools-helpers.js +46 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +78 -12
- package/dist/tools/unadvertised-help.js +4 -4
- package/dist/tools/verify-next-baton.d.ts +53 -0
- package/dist/tools/verify-next-baton.js +52 -0
- package/dist/update/reticle-deps.d.ts +9 -0
- package/dist/update/reticle-deps.js +27 -0
- package/dist/update/sdk-sync.d.ts +1 -2
- package/dist/update/sdk-sync.js +1 -21
- package/dist/update/update-nudge.d.ts +25 -10
- package/dist/update/update-nudge.js +32 -3
- package/dist/update/updater.d.ts +1 -1
- package/dist/version/peer-announce.js +4 -2
- package/dist/version/sdk-fix.d.ts +47 -0
- package/dist/version/sdk-fix.js +127 -0
- package/dist/version/version-nudge.d.ts +14 -0
- package/dist/version/version-nudge.js +21 -0
- package/dist/version/version-skew.d.ts +38 -3
- package/dist/version/version-skew.js +83 -7
- package/dist/visual/visual-store.d.ts +6 -6
- package/dist/visual/visual-store.js +18 -16
- package/dist/visual/visual-tools.js +57 -12
- package/docs/README.md +2 -1
- package/docs/cli/cloud.mdx +1 -1
- package/docs/cli/doctor.mdx +2 -0
- package/docs/cli/serve.mdx +4 -2
- package/docs/cli/verify.mdx +4 -5
- package/docs/cli.mdx +7 -3
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +2 -2
- package/docs/gates.md +1 -1
- package/docs/getting-started.md +19 -3
- package/docs/index.mdx +9 -6
- package/docs/install-agentic.mdx +38 -12
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages.mdx +8 -6
- package/docs/platform-integration.md +17 -0
- package/docs/predicates.mdx +8 -0
- package/docs/reticle-manual.md +106 -0
- package/docs/skill-file.mdx +3 -3
- package/docs/telemetry-contract.md +108 -4
- package/docs/tools-act-sequence.mdx +16 -0
- package/docs/tools-tools-and-run.mdx +2 -4
- package/docs/troubleshooting.mdx +9 -0
- package/docs/usage.md +4 -3
- package/package.json +4 -3
- package/dist/init/cursor.d.ts +0 -24
- package/dist/init/cursor.js +0 -80
- package/dist/session/session-readiness.d.ts +0 -34
- package/dist/session/session-readiness.js +0 -36
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP proxy's POST leg: its own socket pool, and the one retry that is safe to make.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `mcp-proxy.ts` when that file passed the 1000-line backstop after two independent
|
|
5
|
+
* hardenings of this path met in a merge. It earns its own module rather than being an arbitrary
|
|
6
|
+
* slice: everything here is about ONE thing — getting a single JSON-RPC line into the daemon, or
|
|
7
|
+
* saying honestly that it never arrived — and the file it came from is about the SSE stream.
|
|
8
|
+
*/
|
|
9
|
+
import * as http from 'node:http';
|
|
10
|
+
import { log } from '../log.js';
|
|
11
|
+
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
12
|
+
import { reconnectDelayMs } from './proxy-backoff.js';
|
|
13
|
+
/** One bounded pool for the short-lived POST side of each MCP SSE session. */
|
|
14
|
+
export const MCP_PROXY_HTTP_AGENT_OPTIONS = {
|
|
15
|
+
keepAlive: true,
|
|
16
|
+
keepAliveMsecs: 30_000,
|
|
17
|
+
timeout: 60_000,
|
|
18
|
+
maxSockets: 8,
|
|
19
|
+
scheduling: 'lifo',
|
|
20
|
+
};
|
|
21
|
+
const MCP_PROXY_HTTP_AGENT = new http.Agent(MCP_PROXY_HTTP_AGENT_OPTIONS);
|
|
22
|
+
/**
|
|
23
|
+
* POST one JSON-RPC line into the daemon's session. Resolves null on success, or a structured
|
|
24
|
+
* failure when the request never reached a server that will answer it.
|
|
25
|
+
*
|
|
26
|
+
* It used to resolve `void` in every case, logging the failure and moving on. That is the THIRD way
|
|
27
|
+
* a call goes unanswered, and the only one neither `streamLossReplies` nor the queue timer can see:
|
|
28
|
+
* the SSE stream is healthy (so nothing drops) and the request was forwarded (so nothing is queued),
|
|
29
|
+
* but the POST leg is its own TCP connection and an ECONNRESET on it means the daemon never received
|
|
30
|
+
* the call. Nobody was ever going to reply, and the caller waited for its own timeout.
|
|
31
|
+
*/
|
|
32
|
+
const RETRYABLE_UNSENT_POST_ERRORS = new Set(['ENOBUFS', 'ERR_NO_BUFFER_SPACE', 'EADDRNOTAVAIL']);
|
|
33
|
+
export function shouldRetryUnsentPost(error, bytesWritten, retryCount) {
|
|
34
|
+
const code = error?.code;
|
|
35
|
+
return 0 === retryCount && 0 === bytesWritten && RETRYABLE_UNSENT_POST_ERRORS.has(String(code));
|
|
36
|
+
}
|
|
37
|
+
export function postToSession(url, body, request = (options, callback) => http.request(options, callback)) {
|
|
38
|
+
const send = (retryCount) => new Promise((resolve) => {
|
|
39
|
+
const parsed = new URL(url);
|
|
40
|
+
const bodyBuf = Buffer.from(body, 'utf8');
|
|
41
|
+
const options = {
|
|
42
|
+
host: parsed.hostname,
|
|
43
|
+
port: parsed.port !== '' ? parseInt(parsed.port, 10) : 80,
|
|
44
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
45
|
+
method: 'POST',
|
|
46
|
+
agent: MCP_PROXY_HTTP_AGENT,
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
'Content-Length': bodyBuf.byteLength,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
// A keep-alive socket carries historical bytes; retry safety depends on this request's delta.
|
|
53
|
+
let socket;
|
|
54
|
+
let socketBytesBeforeRequest = 0;
|
|
55
|
+
let settled = false;
|
|
56
|
+
const req = request(options, (res) => {
|
|
57
|
+
if (settled)
|
|
58
|
+
return;
|
|
59
|
+
settled = true;
|
|
60
|
+
const status = res.statusCode ?? 0;
|
|
61
|
+
const rejected = status < 200 || status >= 300;
|
|
62
|
+
if (rejected) {
|
|
63
|
+
// A non-2xx from the daemon MCP endpoint used to be swallowed, hanging the JSON-RPC call
|
|
64
|
+
// client-side with no diagnostic. It is a refusal: no response is coming over the stream.
|
|
65
|
+
log('reticle_mcp_proxy_post_non2xx', { status, path: options.path });
|
|
66
|
+
}
|
|
67
|
+
else if (retryCount > 0) {
|
|
68
|
+
getSessionMetrics().recordPostRetrySaved();
|
|
69
|
+
}
|
|
70
|
+
res.resume(); // drain so the socket is reused
|
|
71
|
+
resolve(rejected
|
|
72
|
+
? {
|
|
73
|
+
reason: `daemon rejected the call with HTTP ${String(status)}`,
|
|
74
|
+
transport: false,
|
|
75
|
+
attempts: retryCount + 1,
|
|
76
|
+
}
|
|
77
|
+
: null);
|
|
78
|
+
});
|
|
79
|
+
req.once('socket', (assigned) => {
|
|
80
|
+
socket = assigned;
|
|
81
|
+
socketBytesBeforeRequest = assigned.bytesWritten;
|
|
82
|
+
});
|
|
83
|
+
req.on('error', (err) => {
|
|
84
|
+
if (settled)
|
|
85
|
+
return;
|
|
86
|
+
settled = true;
|
|
87
|
+
// Invisible to mcp_connection_lost (SSE is up) and to tool_refused (handler never ran).
|
|
88
|
+
getSessionMetrics().recordPostSocketFailure();
|
|
89
|
+
const bytesWritten = socket === undefined ? 0 : Math.max(0, socket.bytesWritten - socketBytesBeforeRequest);
|
|
90
|
+
if (shouldRetryUnsentPost(err, bytesWritten, retryCount)) {
|
|
91
|
+
const retryInMs = reconnectDelayMs(retryCount + 1);
|
|
92
|
+
log('reticle_mcp_proxy_post_retry', {
|
|
93
|
+
code: err.code,
|
|
94
|
+
retryInMs,
|
|
95
|
+
});
|
|
96
|
+
setTimeout(() => void send(retryCount + 1).then(resolve), retryInMs);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
log('reticle_mcp_proxy_post_error', { error: err.message });
|
|
100
|
+
resolve({
|
|
101
|
+
reason: `post failed: ${err.message}`,
|
|
102
|
+
transport: true,
|
|
103
|
+
attempts: retryCount + 1,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
req.end(bodyBuf);
|
|
107
|
+
});
|
|
108
|
+
return send(0);
|
|
109
|
+
}
|
package/dist/mcp/mcp-proxy.d.ts
CHANGED
|
@@ -5,13 +5,14 @@ export { probeDaemon, waitForDaemon } from './proxy-daemon-probe.js';
|
|
|
5
5
|
* proxy. Where the code lives is a file-size decision; where it is imported from is an API.
|
|
6
6
|
*/
|
|
7
7
|
export { accountProxyLogWrite, proxyLog, proxyLogPath, PROXY_LOG_CHECK_BYTES, recoverOversizedProxyLog, setProxyLogPort, } from './proxy-log.js';
|
|
8
|
+
export { reconnectDelayMs, RECONNECT_BASE_MS, RECONNECT_CAP_MS } from './proxy-backoff.js';
|
|
9
|
+
export { MCP_PROXY_HTTP_AGENT_OPTIONS, postToSession, shouldRetryUnsentPost, type PostFailure, } from './mcp-post-transport.js';
|
|
8
10
|
/**
|
|
9
11
|
* Overridable so the survival spec can reach exhaustion in seconds instead of minutes — the same
|
|
10
12
|
* reason the idle windows take an env override. A positive integer only; anything else falls back,
|
|
11
13
|
* so a typo cannot silently disable the retry loop.
|
|
12
14
|
*/
|
|
13
15
|
export declare const MAX_RECONNECT_ATTEMPTS: number;
|
|
14
|
-
export declare function reconnectDelayMs(attempt: number): number;
|
|
15
16
|
/** JSON-RPC id the proxy uses for its own replayed `initialize` — never one the client could send. */
|
|
16
17
|
export declare const RECONNECT_INITIALIZE_ID = "__reticle_proxy_reinit";
|
|
17
18
|
/**
|
|
@@ -47,13 +48,6 @@ export declare class PendingRequests {
|
|
|
47
48
|
* with the MCP server still perfectly alive. An agent cannot tell that apart from "still working".
|
|
48
49
|
*/
|
|
49
50
|
export declare function streamLossReplies(pending: PendingRequests, reason: string, nextStep?: string): string[];
|
|
50
|
-
/**
|
|
51
|
-
* The one shape every "the transport ate your call" answer takes, whichever leg died.
|
|
52
|
-
*
|
|
53
|
-
* `nextStep` is appended when the caller knows one. Without it this reply describes a condition and
|
|
54
|
-
* stops there, which is all a first-run user got out of their first ever tool call.
|
|
55
|
-
*/
|
|
56
|
-
export declare function transportLossReply(id: unknown, reason: string, nextStep?: string): string;
|
|
57
51
|
/**
|
|
58
52
|
* How long a request may sit in the queue, unsent, before the proxy answers it itself.
|
|
59
53
|
*
|
package/dist/mcp/mcp-proxy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as http from 'node:http';
|
|
2
|
-
import { localInitializeResponse, isHandshakeLine, drainLines, MAX_STDIN_LINE_BYTES, } from './proxy-handshake.js';
|
|
3
|
-
import { ToolCatalogCache } from './tool-catalog-cache.js';
|
|
2
|
+
import { localInitializeResponse, degradedInstructions, isHandshakeLine, drainLines, MAX_STDIN_LINE_BYTES, } from './proxy-handshake.js';
|
|
3
|
+
import { isToolCallRequest, isToolsListRequest, ToolCatalogCache } from './tool-catalog-cache.js';
|
|
4
|
+
import { rememberEnumerated, rememberProxyStarted, rememberToolCalled } from './attach-memory.js';
|
|
4
5
|
import { toolsChangedNotification } from './proxy-handshake.js';
|
|
5
6
|
import { LOOPBACK_HOST, MCP_SSE_PATH, MCP_SHUTDOWN_EVENT, ReticleEnv, CONTRACT_FINGERPRINT, } from '@reticlehq/core';
|
|
6
7
|
import { SseFrameParser } from './sse-frame-parser.js';
|
|
@@ -15,6 +16,7 @@ import { readProjectId } from '../cli/cli-port.js';
|
|
|
15
16
|
import { PEER_VERSION_PARAM, PEER_CONTRACT_PARAM } from '../version/peer-announce.js';
|
|
16
17
|
import { onStreamDrop, onClientRequest, onReconnectBudgetSpent, onQueueExpired, OnDrop, OnRequest, } from './proxy-lifecycle.js';
|
|
17
18
|
import { describePresence, probePresence } from '../daemon/port-presence.js';
|
|
19
|
+
import { flushProxySessionMetrics } from '../telemetry/proxy-telemetry.js';
|
|
18
20
|
/**
|
|
19
21
|
* The same `/status` probe `doctor`, `status` and `kill` ask with. Reused rather than re-written:
|
|
20
22
|
* the whole defect this import closes was the proxy answering a DIFFERENT question from every other
|
|
@@ -27,11 +29,11 @@ import { fetchStatus as fetchDaemonStatus } from '../cli/cli-launch.js';
|
|
|
27
29
|
*/
|
|
28
30
|
export { accountProxyLogWrite, proxyLog, proxyLogPath, PROXY_LOG_CHECK_BYTES, recoverOversizedProxyLog, setProxyLogPort, } from './proxy-log.js';
|
|
29
31
|
import { proxyLog } from './proxy-log.js';
|
|
30
|
-
import { log } from '../log.js';
|
|
31
32
|
import { OutageReason, OutageStage, reportMcpOutage } from './mcp-outage.js';
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
import { postToSession } from './mcp-post-transport.js';
|
|
34
|
+
import { reconnectDelayMs } from './proxy-backoff.js';
|
|
35
|
+
export { reconnectDelayMs, RECONNECT_BASE_MS, RECONNECT_CAP_MS } from './proxy-backoff.js';
|
|
36
|
+
export { MCP_PROXY_HTTP_AGENT_OPTIONS, postToSession, shouldRetryUnsentPost, } from './mcp-post-transport.js';
|
|
35
37
|
/**
|
|
36
38
|
* How many consecutive failed reconnects before the proxy stops RETRYING. It does not stop serving:
|
|
37
39
|
* the budget ends the retry loop and the proxy goes dormant, where the handshake and `tools/list`
|
|
@@ -50,9 +52,6 @@ export const MAX_RECONNECT_ATTEMPTS = (() => {
|
|
|
50
52
|
const raw = Number(process.env[ReticleEnv.RECONNECT_ATTEMPTS]);
|
|
51
53
|
return Number.isInteger(raw) && raw > 0 ? raw : DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
52
54
|
})();
|
|
53
|
-
export function reconnectDelayMs(attempt) {
|
|
54
|
-
return Math.min(RECONNECT_BASE_MS * attempt, RECONNECT_CAP_MS);
|
|
55
|
-
}
|
|
56
55
|
/** JSON-RPC id the proxy uses for its own replayed `initialize` — never one the client could send. */
|
|
57
56
|
export const RECONNECT_INITIALIZE_ID = '__reticle_proxy_reinit';
|
|
58
57
|
function parseJsonRpc(line) {
|
|
@@ -141,7 +140,7 @@ const TRANSPORT_LOSS_CODE = -32001;
|
|
|
141
140
|
* `nextStep` is appended when the caller knows one. Without it this reply describes a condition and
|
|
142
141
|
* stops there, which is all a first-run user got out of their first ever tool call.
|
|
143
142
|
*/
|
|
144
|
-
|
|
143
|
+
function transportLossReply(id, reason, nextStep) {
|
|
145
144
|
return JSON.stringify({
|
|
146
145
|
jsonrpc: '2.0',
|
|
147
146
|
id,
|
|
@@ -253,49 +252,6 @@ export class HandshakeReplay {
|
|
|
253
252
|
return true;
|
|
254
253
|
}
|
|
255
254
|
}
|
|
256
|
-
/**
|
|
257
|
-
* POST one JSON-RPC line into the daemon's session. Resolves null on success, or a short reason
|
|
258
|
-
* when the request never reached a server that will answer it.
|
|
259
|
-
*
|
|
260
|
-
* It used to resolve `void` in every case, logging the failure and moving on. That is the THIRD way
|
|
261
|
-
* a call goes unanswered, and the only one neither `streamLossReplies` nor the queue timer can see:
|
|
262
|
-
* the SSE stream is healthy (so nothing drops) and the request was forwarded (so nothing is queued),
|
|
263
|
-
* but the POST leg is its own TCP connection and an ECONNRESET on it means the daemon never received
|
|
264
|
-
* the call. Nobody was ever going to reply, and the caller waited for its own timeout.
|
|
265
|
-
*/
|
|
266
|
-
function postToSession(url, body) {
|
|
267
|
-
return new Promise((resolve) => {
|
|
268
|
-
const parsed = new URL(url);
|
|
269
|
-
const bodyBuf = Buffer.from(body, 'utf8');
|
|
270
|
-
const options = {
|
|
271
|
-
host: parsed.hostname,
|
|
272
|
-
port: parsed.port !== '' ? parseInt(parsed.port, 10) : 80,
|
|
273
|
-
path: `${parsed.pathname}${parsed.search}`,
|
|
274
|
-
method: 'POST',
|
|
275
|
-
headers: {
|
|
276
|
-
'Content-Type': 'application/json',
|
|
277
|
-
'Content-Length': bodyBuf.byteLength,
|
|
278
|
-
},
|
|
279
|
-
};
|
|
280
|
-
const req = http.request(options, (res) => {
|
|
281
|
-
const status = res.statusCode ?? 0;
|
|
282
|
-
const rejected = status < 200 || status >= 300;
|
|
283
|
-
if (rejected) {
|
|
284
|
-
// A non-2xx from the daemon MCP endpoint used to be swallowed, hanging the JSON-RPC call
|
|
285
|
-
// client-side with no diagnostic. It is a refusal: no response is coming over the stream.
|
|
286
|
-
log('reticle_mcp_proxy_post_non2xx', { status, path: options.path });
|
|
287
|
-
}
|
|
288
|
-
res.resume(); // drain so the socket is reused
|
|
289
|
-
resolve(rejected ? `daemon rejected the call with HTTP ${String(status)}` : null);
|
|
290
|
-
});
|
|
291
|
-
req.on('error', (err) => {
|
|
292
|
-
log('reticle_mcp_proxy_post_error', { error: err.message });
|
|
293
|
-
resolve(`post failed: ${err.message}`);
|
|
294
|
-
});
|
|
295
|
-
req.write(bodyBuf);
|
|
296
|
-
req.end();
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
255
|
/**
|
|
300
256
|
* Turn an `endpoint` frame's data into a POST target, or null when there is nothing usable in it.
|
|
301
257
|
*
|
|
@@ -347,6 +303,11 @@ export function startMcpProxy(port,
|
|
|
347
303
|
*/
|
|
348
304
|
ensureDaemon) {
|
|
349
305
|
return new Promise((_resolve, reject) => {
|
|
306
|
+
// A client just started us, which is the only honest evidence that Reticle is registered with
|
|
307
|
+
// one at all: a config entry proves somebody wrote JSON, not that a client ever read it. Paired
|
|
308
|
+
// with the `tools/list` record below, this is what lets `status` name the state in between
|
|
309
|
+
// "never registered" and "connected" — see attach-memory.ts.
|
|
310
|
+
rememberProxyStarted(reticleStateHome(), port);
|
|
350
311
|
// Declared before the SSE handler that fills it: the handler is a closure created below but
|
|
351
312
|
// invoked on the first daemon frame, and a `const` referenced before its declaration executes is
|
|
352
313
|
// a runtime throw, not a type error.
|
|
@@ -433,6 +394,17 @@ ensureDaemon) {
|
|
|
433
394
|
let daemonRetiring = false;
|
|
434
395
|
/** Why the stream we are currently trying to replace went away — carried onto the recovery. */
|
|
435
396
|
let lastDropReason = OutageReason.OTHER;
|
|
397
|
+
/**
|
|
398
|
+
* What the outage had already cost when the proxy went dormant.
|
|
399
|
+
*
|
|
400
|
+
* The wake path clears `attempts` before dialling, and it must: a wake is a fresh retry budget,
|
|
401
|
+
* and sharing the previous outage's spent budget would put the proxy dormant again on its first
|
|
402
|
+
* failure. But the recovery report is guarded on `attempts > 0`, so clearing it also erased the
|
|
403
|
+
* evidence that there was anything to recover FROM — and dormancy is the commonest outage class
|
|
404
|
+
* we have, which made every recovery of that class invisible. Carried across the wake so the
|
|
405
|
+
* budget resets and the accounting does not.
|
|
406
|
+
*/
|
|
407
|
+
let attemptsBeforeDormant = 0;
|
|
436
408
|
/**
|
|
437
409
|
* When the proxy started, and how many times the first connect has been refused since.
|
|
438
410
|
*
|
|
@@ -455,13 +427,20 @@ ensureDaemon) {
|
|
|
455
427
|
const msg = parseJsonRpc(line);
|
|
456
428
|
if (null === msg || msg.id === undefined || !pending.take(msg.id))
|
|
457
429
|
return;
|
|
430
|
+
if (failure.transport) {
|
|
431
|
+
reportMcpOutage(OutageStage.FIRST, {
|
|
432
|
+
reason: OutageReason.CONNECT_ERROR,
|
|
433
|
+
attempts: failure.attempts,
|
|
434
|
+
pendingLost: 1,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
458
437
|
proxyLog('reticle_mcp_proxy_post_unanswered', {
|
|
459
438
|
port,
|
|
460
439
|
method: String(msg.method),
|
|
461
|
-
reason: failure,
|
|
440
|
+
reason: failure.reason,
|
|
462
441
|
note: 'the stream is still up, so nothing else would ever have answered this call',
|
|
463
442
|
});
|
|
464
|
-
emit(transportLossReply(msg.id, failure));
|
|
443
|
+
emit(transportLossReply(msg.id, failure.reason));
|
|
465
444
|
});
|
|
466
445
|
};
|
|
467
446
|
function onSseEvent(event, data, p) {
|
|
@@ -481,9 +460,11 @@ ensureDaemon) {
|
|
|
481
460
|
// off and never reaching the budget that puts the proxy dormant. Measured: 32 retries in 8s.
|
|
482
461
|
// Report the recovery BEFORE the counter is cleared: `attempts` is what coming back actually
|
|
483
462
|
// cost, and it is the only number that can falsify the drop event. See OutageStage.RECOVERED.
|
|
484
|
-
|
|
485
|
-
|
|
463
|
+
const cost = attempts + attemptsBeforeDormant;
|
|
464
|
+
if (cost > 0)
|
|
465
|
+
reportMcpOutage(OutageStage.RECOVERED, { reason: lastDropReason, attempts: cost });
|
|
486
466
|
attempts = 0;
|
|
467
|
+
attemptsBeforeDormant = 0;
|
|
487
468
|
// The new session's McpServer has never seen the client's initialize — replay it first, then
|
|
488
469
|
// flush whatever the client sent while we were reconnecting.
|
|
489
470
|
for (const line of replay.replayLines())
|
|
@@ -704,10 +685,10 @@ ensureDaemon) {
|
|
|
704
685
|
const armLocalHandshake = (line) => {
|
|
705
686
|
if (handshakeAnswered)
|
|
706
687
|
return;
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
|
|
710
|
-
if (null ===
|
|
688
|
+
// Only an `initialize` REQUEST is ours to answer. Asked with no instructions because the ones
|
|
689
|
+
// we will actually send name the reason the daemon failed, and the reason is not known until
|
|
690
|
+
// the deliver closure runs — see degradedInstructions.
|
|
691
|
+
if (null === localInitializeResponse(line, ''))
|
|
711
692
|
return;
|
|
712
693
|
// Take the debt NOW, not when the timer fires.
|
|
713
694
|
//
|
|
@@ -726,6 +707,13 @@ ensureDaemon) {
|
|
|
726
707
|
deliverHandshakeLocally = (reason) => {
|
|
727
708
|
if (handshakeAnswered || postUrl !== null)
|
|
728
709
|
return;
|
|
710
|
+
// Built here, not at arm time, so the notice can name WHY no daemon answered. The client
|
|
711
|
+
// reads `instructions` exactly once, at initialize, and this response is the only one it
|
|
712
|
+
// will ever get — so a handshake that does not say it is unbacked never says it at all.
|
|
713
|
+
// See degradedInstructions for the field report this closes.
|
|
714
|
+
const response = localInitializeResponse(line, degradedInstructions(proxyInstructions(port), port, reason));
|
|
715
|
+
if (null === response)
|
|
716
|
+
return;
|
|
729
717
|
handshakeAnswered = true;
|
|
730
718
|
deliverHandshakeLocally = undefined;
|
|
731
719
|
// The catalog we are about to serve is ours, not the daemon's, so it has to be corrected
|
|
@@ -767,6 +755,17 @@ ensureDaemon) {
|
|
|
767
755
|
continue;
|
|
768
756
|
replay.observeOutbound(trimmed);
|
|
769
757
|
pending.observeOutbound(trimmed);
|
|
758
|
+
// The ARRIVAL of the request, not the delivery of an answer: "this client asked for the tool
|
|
759
|
+
// list" is the fact `status` reports on, and a request that arrived and then failed is a
|
|
760
|
+
// different problem with its own diagnosis. Recording the answer instead would let a host
|
|
761
|
+
// that never asks look identical to one that asked and was refused.
|
|
762
|
+
if (isToolsListRequest(trimmed))
|
|
763
|
+
rememberEnumerated(reticleStateHome(), port);
|
|
764
|
+
// The hop every other check misses. Recorded on the REQUEST rather than a reply, because
|
|
765
|
+
// the question is whether anything ever left the agent — a call that leaves and gets no
|
|
766
|
+
// answer is a different fault from one that was never made, and only this side sees it.
|
|
767
|
+
if (isToolCallRequest(trimmed))
|
|
768
|
+
rememberToolCalled(reticleStateHome(), port);
|
|
770
769
|
const action = onClientRequest(postUrl !== null, dormant);
|
|
771
770
|
if (action === OnRequest.SEND && postUrl !== null) {
|
|
772
771
|
forward(postUrl, trimmed);
|
|
@@ -793,6 +792,8 @@ ensureDaemon) {
|
|
|
793
792
|
// flushed once the new session's `endpoint` frame arrives.
|
|
794
793
|
if (action === OnRequest.WAKE) {
|
|
795
794
|
dormant = false;
|
|
795
|
+
// Budget reset, accounting preserved. See attemptsBeforeDormant.
|
|
796
|
+
attemptsBeforeDormant += attempts;
|
|
796
797
|
attempts = 0;
|
|
797
798
|
void (ensureDaemon?.() ?? Promise.resolve())
|
|
798
799
|
.then(() => connect(false))
|
|
@@ -820,7 +821,8 @@ ensureDaemon) {
|
|
|
820
821
|
process.stdin.on('end', () => {
|
|
821
822
|
const quit = (code) => {
|
|
822
823
|
stopped = true;
|
|
823
|
-
|
|
824
|
+
// Await: fire-and-forget here is how daemon_stopped never arrived.
|
|
825
|
+
void flushProxySessionMetrics().finally(() => process.exit(code));
|
|
824
826
|
};
|
|
825
827
|
if (0 === pending.unanswered.length) {
|
|
826
828
|
quit(0);
|
package/dist/mcp/mcp.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { z } from 'zod';
|
|
|
3
3
|
import { type ToolDeps } from '../tools/tools.js';
|
|
4
4
|
import type { ToolDef } from '../tools/tools.js';
|
|
5
5
|
import { type ToolSurface } from '../tools/tool-surface.js';
|
|
6
|
+
/** The JSON-RPC method the SDK registers its tool dispatcher under. */
|
|
7
|
+
export declare const CALL_TOOL_METHOD = "tools/call";
|
|
6
8
|
/**
|
|
7
9
|
* Merge the runtime-spliced envelope (health/lease/age/control) into a session-bound tool's declared
|
|
8
10
|
* outputSchema so structuredContent validation keeps those fields instead of dropping them. The tool's
|
package/dist/mcp/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import {
|
|
4
|
+
import { isPredicateParam } from '../events/predicate-eval.js';
|
|
5
5
|
import { isToonable, resultToToon, MCP_SERVER_NAME } from '@reticlehq/core';
|
|
6
6
|
import { TOOLS } from '../tools/tools.js';
|
|
7
7
|
import { filterTools, describeToolSurface, TOOL_SURFACE, } from '../tools/tool-surface.js';
|
|
@@ -11,11 +11,24 @@ import { buildDynamicTools } from '../tools/dynamic-tools.js';
|
|
|
11
11
|
import { runTool, SESSION_BOUND_TOOLS } from '../tools/invoke-tool.js';
|
|
12
12
|
import { sessionEnvelopeShape } from '../tools/tool-kit.js';
|
|
13
13
|
import { buildErrorPayload } from '../tools/error-recovery.js';
|
|
14
|
+
import { takeVersionSkewOnto } from '../version/version-nudge.js';
|
|
14
15
|
import { resultIsError } from './mcp-is-error.js';
|
|
15
16
|
import { buildServerInstructions } from './server-instructions.js';
|
|
16
17
|
import { unadvertisedToolHelp } from '../tools/unadvertised-help.js';
|
|
17
18
|
/** The JSON-RPC method the SDK registers its tool dispatcher under. */
|
|
18
|
-
const CALL_TOOL_METHOD = 'tools/call';
|
|
19
|
+
export const CALL_TOOL_METHOD = 'tools/call';
|
|
20
|
+
/**
|
|
21
|
+
* What we say when an SDK internal these patches ride on is no longer there.
|
|
22
|
+
*
|
|
23
|
+
* LOG rather than THROW, deliberately. Both patches improve an ERROR MESSAGE; neither is load
|
|
24
|
+
* bearing for a single tool call, and a throw here runs at server construction, so a renamed
|
|
25
|
+
* internal in a minor SDK bump would take the whole MCP server down for every user rather than
|
|
26
|
+
* degrade one message. That trade is backwards. The place a shape change must fail is the gate —
|
|
27
|
+
* `sdk-private-shape.test.ts` asserts both shapes and reddens CI on the bump — and this line is the
|
|
28
|
+
* backstop for the one case the gate cannot see: a user whose installed SDK is not the one CI
|
|
29
|
+
* resolved. The old behaviour, a bare `return`, gave neither.
|
|
30
|
+
*/
|
|
31
|
+
const SDK_SHAPE_MISSING = 'sdk_internal_missing';
|
|
19
32
|
import { log } from '../log.js';
|
|
20
33
|
import { SERVER_VERSION } from '../version/server-version.js';
|
|
21
34
|
import { setMcpClientNameHook } from '../telemetry/feedback-context.js';
|
|
@@ -73,23 +86,6 @@ export function firstSentence(description) {
|
|
|
73
86
|
* Across the three tools that take a predicate that is 72% of the entire advertised input schema — a
|
|
74
87
|
* cost re-sent on every request, forever, to describe a grammar most calls use one variant of.
|
|
75
88
|
*/
|
|
76
|
-
/**
|
|
77
|
-
* Is this parameter ACTUALLY the predicate union?
|
|
78
|
-
*
|
|
79
|
-
* This used to be a name check — `new Set(['predicate', 'until'])` — and `until` is overloaded on
|
|
80
|
-
* this surface: the act/assert family means a predicate by it, while reticle_observe / _network /
|
|
81
|
-
* _console mean a NUMBER, an upper cursor bound. So all three had their numeric parameter advertised
|
|
82
|
-
* to the agent as `Predicate object: { kind, ...fields }`, typed as a record, under the DEFAULT
|
|
83
|
-
* profile. An agent believing the schema passes an object, `asNumber` yields undefined, the bound is
|
|
84
|
-
* silently dropped and the tool answers over the wrong window — a wrong answer wearing the shape of
|
|
85
|
-
* an answer.
|
|
86
|
-
*
|
|
87
|
-
* Keying on the schema cannot make that mistake: a parameter is compacted because it IS a predicate.
|
|
88
|
-
*/
|
|
89
|
-
function isPredicateParam(schema) {
|
|
90
|
-
const inner = schema instanceof z.ZodOptional ? schema.unwrap() : schema;
|
|
91
|
-
return inner === PredicateSchema || inner === PredicateSchema.optional();
|
|
92
|
-
}
|
|
93
89
|
/**
|
|
94
90
|
* What a lean profile advertises in place of the full predicate union.
|
|
95
91
|
*
|
|
@@ -246,7 +242,12 @@ export function advertisedConfig(tool, advertised, profile) {
|
|
|
246
242
|
// `verify` is lean for the same reason `default` is, and more so: it exists to make a verdict
|
|
247
243
|
// cheap, and it was serving FULL descriptions plus outputSchema because the check named only the
|
|
248
244
|
// default. That put 8,207 bytes on the wire for three tools.
|
|
249
|
-
|
|
245
|
+
// `lean` here is the ADJECTIVE (terse descriptions, no output schemas), which every trimmed surface
|
|
246
|
+
// wants — including TOOL_SURFACE.LEAN, the profile that borrows the word. A trimmed surface serving
|
|
247
|
+
// full prose plus output schemas is the exact defect that put 8,207 bytes on `verify`'s three tools.
|
|
248
|
+
const lean = profile === TOOL_SURFACE.DEFAULT ||
|
|
249
|
+
profile === TOOL_SURFACE.VERIFY ||
|
|
250
|
+
profile === TOOL_SURFACE.LEAN;
|
|
250
251
|
const terse = lean && !isMetaTool;
|
|
251
252
|
// The first advertised tool carrying a predicate spells the grammar out; the rest point at it.
|
|
252
253
|
const anchor = advertised.find((t) => Object.values(t.inputSchema).some((schema) => isPredicateParam(schema)))?.name;
|
|
@@ -270,8 +271,10 @@ function unknownKeys(args, allowed) {
|
|
|
270
271
|
export function installFriendlyArgErrors(server, examples, allowedKeys = new Map()) {
|
|
271
272
|
const target = server;
|
|
272
273
|
const original = target.validateToolInput;
|
|
273
|
-
if (typeof original !== 'function')
|
|
274
|
+
if (typeof original !== 'function') {
|
|
275
|
+
log(SDK_SHAPE_MISSING, { internal: 'validateToolInput', lost: 'friendly argument errors' });
|
|
274
276
|
return;
|
|
277
|
+
}
|
|
275
278
|
target.validateToolInput = async (tool, args, toolName) => {
|
|
276
279
|
// An UNKNOWN key is the failure zod cannot catch, because object schemas are non-strict: the key
|
|
277
280
|
// is silently dropped and the tool answers as if it had not been asked. Measured live —
|
|
@@ -281,8 +284,14 @@ export function installFriendlyArgErrors(server, examples, allowedKeys = new Map
|
|
|
281
284
|
const unknown = unknownKeys(args, allowedKeys.get(toolName));
|
|
282
285
|
if (unknown.length > 0) {
|
|
283
286
|
const example = examples.get(toolName);
|
|
287
|
+
// What the tool DOES declare, in the same breath as what it does not. Naming only the bad key
|
|
288
|
+
// leaves the caller guessing again, and the next guess costs another round trip — which is the
|
|
289
|
+
// whole shape of the failure being fixed here. The list is already in hand, and for a tool
|
|
290
|
+
// with no example (the meta-tools) it is the only thing standing between a typo and a retry.
|
|
291
|
+
const declared = [...(allowedKeys.get(toolName) ?? [])].join(', ');
|
|
284
292
|
throw new McpError(ErrorCode.InvalidParams, `Unknown ${1 === unknown.length ? 'parameter' : 'parameters'} for ${toolName}: ${unknown.join(', ')}. ` +
|
|
285
293
|
`They were NOT applied — a result computed without them would look like an answer. ` +
|
|
294
|
+
(0 === declared.length ? '' : `${toolName} declares: ${declared}. `) +
|
|
286
295
|
(example === undefined
|
|
287
296
|
? `Call reticle_tools { names: ["${toolName}"] } for its parameters.`
|
|
288
297
|
: `A valid call looks like: ${toolName} ${example}`));
|
|
@@ -347,8 +356,13 @@ function installUnadvertisedToolHelp(server, advertised, known) {
|
|
|
347
356
|
const inner = server.server;
|
|
348
357
|
const handlers = inner._requestHandlers;
|
|
349
358
|
const original = handlers?.get(CALL_TOOL_METHOD);
|
|
350
|
-
if (handlers === undefined || original === undefined)
|
|
359
|
+
if (handlers === undefined || original === undefined) {
|
|
360
|
+
log(SDK_SHAPE_MISSING, {
|
|
361
|
+
internal: handlers === undefined ? '_requestHandlers' : CALL_TOOL_METHOD,
|
|
362
|
+
lost: 'unadvertised-tool help',
|
|
363
|
+
});
|
|
351
364
|
return;
|
|
365
|
+
}
|
|
352
366
|
handlers.set(CALL_TOOL_METHOD, async (request, extra) => {
|
|
353
367
|
const name = toolNameOf(request);
|
|
354
368
|
const help = name === undefined ? undefined : unadvertisedToolHelp(name, advertised, known);
|
|
@@ -439,7 +453,16 @@ tools = TOOLS) {
|
|
|
439
453
|
.map((tool) => [tool.name, JSON.stringify(tool.example)])),
|
|
440
454
|
// Every tool in the table, not just the advertised ones: reticle_run dispatches to the full
|
|
441
455
|
// table, so an unadvertised tool reached through the hatch must be just as strict.
|
|
442
|
-
|
|
456
|
+
//
|
|
457
|
+
// ...and the advertised list on top of it, because the two META-tools are not in the table — they
|
|
458
|
+
// are composed onto the surface — so they had no entry, and no entry means no declared keys means
|
|
459
|
+
// every key allowed. `reticle_tools` took a misnamed argument, dropped it, and answered with the
|
|
460
|
+
// whole catalogue: a well-formed answer to a question nobody asked, on the one tool whose entire
|
|
461
|
+
// job is telling an agent what the surface is.
|
|
462
|
+
new Map([...tools, ...advertised].map((tool) => [
|
|
463
|
+
tool.name,
|
|
464
|
+
new Set(Object.keys(tool.inputSchema)),
|
|
465
|
+
])));
|
|
443
466
|
for (const tool of advertised) {
|
|
444
467
|
// Output schemas are now the largest slice of the per-request tax (55.6% of the hybrid payload
|
|
445
468
|
// after the predicate fix) and we are the only one of the three MCP servers that sends them.
|
|
@@ -492,7 +515,12 @@ tools = TOOLS) {
|
|
|
492
515
|
// Every error the agent hits should answer "what next?", not just "what broke".
|
|
493
516
|
return {
|
|
494
517
|
isError: true,
|
|
495
|
-
content: [
|
|
518
|
+
content: [
|
|
519
|
+
{
|
|
520
|
+
type: 'text',
|
|
521
|
+
text: JSON.stringify(takeVersionSkewOnto(buildErrorPayload(message))),
|
|
522
|
+
},
|
|
523
|
+
],
|
|
496
524
|
};
|
|
497
525
|
}
|
|
498
526
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconnect backoff, shared by the SSE stream and the POST leg.
|
|
3
|
+
*
|
|
4
|
+
* Its own module only because both halves need it and they must not import each other: the proxy
|
|
5
|
+
* owns the stream, the post transport owns the request, and a cycle between them to reach three
|
|
6
|
+
* lines of arithmetic would be the wrong price.
|
|
7
|
+
*/
|
|
8
|
+
/** Linear and capped, so a briefly-restarting daemon is picked up fast. */
|
|
9
|
+
export declare const RECONNECT_BASE_MS = 250;
|
|
10
|
+
export declare const RECONNECT_CAP_MS = 5000;
|
|
11
|
+
export declare function reconnectDelayMs(attempt: number): number;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconnect backoff, shared by the SSE stream and the POST leg.
|
|
3
|
+
*
|
|
4
|
+
* Its own module only because both halves need it and they must not import each other: the proxy
|
|
5
|
+
* owns the stream, the post transport owns the request, and a cycle between them to reach three
|
|
6
|
+
* lines of arithmetic would be the wrong price.
|
|
7
|
+
*/
|
|
8
|
+
/** Linear and capped, so a briefly-restarting daemon is picked up fast. */
|
|
9
|
+
export const RECONNECT_BASE_MS = 250;
|
|
10
|
+
export const RECONNECT_CAP_MS = 5_000;
|
|
11
|
+
export function reconnectDelayMs(attempt) {
|
|
12
|
+
return Math.min(RECONNECT_BASE_MS * attempt, RECONNECT_CAP_MS);
|
|
13
|
+
}
|
|
@@ -11,9 +11,13 @@ const DAEMON_READY_TIMEOUT_MS = Number.isFinite(envDaemonReadyTimeoutMs) && envD
|
|
|
11
11
|
? envDaemonReadyTimeoutMs
|
|
12
12
|
: DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
13
13
|
const DAEMON_POLL_INTERVAL_MS = 100;
|
|
14
|
+
const DAEMON_POLL_MAX_INTERVAL_MS = 1_000;
|
|
14
15
|
function delay(ms) {
|
|
15
16
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
17
|
}
|
|
18
|
+
export function daemonPollDelayMs(attempt) {
|
|
19
|
+
return Math.min(DAEMON_POLL_INTERVAL_MS * attempt, DAEMON_POLL_MAX_INTERVAL_MS);
|
|
20
|
+
}
|
|
17
21
|
/**
|
|
18
22
|
* Returns true if something is already listening on the reticle port.
|
|
19
23
|
* Uses a plain TCP probe so we don't create a side-effectful SSE session
|
|
@@ -38,11 +42,13 @@ export function probeDaemon(port) {
|
|
|
38
42
|
/** Poll until the daemon's HTTP port accepts connections or the deadline is reached. */
|
|
39
43
|
export async function waitForDaemon(port) {
|
|
40
44
|
const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
|
|
45
|
+
let attempt = 0;
|
|
41
46
|
while (Date.now() < deadline) {
|
|
42
47
|
const reachable = await probeDaemon(port);
|
|
43
48
|
if (reachable)
|
|
44
49
|
return;
|
|
45
|
-
|
|
50
|
+
attempt++;
|
|
51
|
+
await delay(daemonPollDelayMs(attempt));
|
|
46
52
|
}
|
|
47
53
|
throw new Error(`reticle daemon did not become ready on port ${port} within ${DAEMON_READY_TIMEOUT_MS}ms`);
|
|
48
54
|
}
|
|
@@ -44,6 +44,29 @@ instructions?: string): string | null;
|
|
|
44
44
|
* where it is honoured and costs one line where it is not.
|
|
45
45
|
*/
|
|
46
46
|
export declare function toolsChangedNotification(catalogWasLocal: boolean): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* What a locally-answered handshake must say about itself.
|
|
49
|
+
*
|
|
50
|
+
* The proxy answering `initialize` is right — a hang gives the agent no tools AND no diagnosis — but
|
|
51
|
+
* the response it sent was INDISTINGUISHABLE from a daemon's. Same `serverInfo`, same capabilities,
|
|
52
|
+
* same `instructions`. So the client marked the server installed and enabled, the agent got a tool
|
|
53
|
+
* list that was cached at best and empty at worst, and nothing anywhere connected the two facts.
|
|
54
|
+
*
|
|
55
|
+
* Reported from the field, from two different agent hosts at once: the marketplace showed Reticle
|
|
56
|
+
* installed with the whole tool surface enabled, and the agent asked to verify a flow reported that
|
|
57
|
+
* RETICLE WAS NOT PRESENT ON THE MACHINE — while the daemon was running and a browser session was
|
|
58
|
+
* live. That conclusion is the reasonable reading of an empty surface with no explanation attached,
|
|
59
|
+
* and it is unrecoverable: the proxy's own recovery is "the next client request re-probes the port",
|
|
60
|
+
* and a client holding zero tools never makes another request.
|
|
61
|
+
*
|
|
62
|
+
* `instructions` is the channel because it is the only one that survives an empty catalog. It rides
|
|
63
|
+
* on the initialize result, which SUCCEEDED, and every host we target puts it in front of the model.
|
|
64
|
+
* A tool description cannot reach an agent that was handed no tools.
|
|
65
|
+
*
|
|
66
|
+
* The healthy block is kept underneath rather than replaced: an agent that IS about to get a working
|
|
67
|
+
* surface (the common case — the first call wakes a daemon) still needs to know how to drive it.
|
|
68
|
+
*/
|
|
69
|
+
export declare function degradedInstructions(healthy: string, port: number, reason: string): string;
|
|
47
70
|
/**
|
|
48
71
|
* The largest single JSON-RPC line the proxy will accumulate from a client.
|
|
49
72
|
*
|