@reticlehq/server 2.12.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 +67 -109
- 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 -1
- package/dist/bridge/bridge.js +247 -164
- 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 +6 -6
- package/dist/capsule/causal-summary.js +7 -2
- package/dist/cli/cli-doctor.js +31 -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 +15 -1
- package/dist/cli/cli-parse.js +212 -8
- package/dist/cli/cli-verify.d.ts +1 -0
- package/dist/cli/cli-verify.js +3 -2
- package/dist/cli/cloud-cli.js +428 -259
- 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.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 +9 -0
- package/dist/cli/doctor-rows.js +9 -0
- 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/mcp-command.js +51 -20
- package/dist/cli/project-name.d.ts +30 -0
- package/dist/cli/project-name.js +44 -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 +134 -80
- package/dist/cloud/cloud-config.d.ts +22 -1
- package/dist/cloud/cloud-config.js +64 -5
- package/dist/cloud/cloud-sync.d.ts +5 -5
- package/dist/cloud/cloud-sync.js +33 -2
- 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 +3 -0
- package/dist/crawl/crawl.js +5 -0
- package/dist/daemon/daemon-resolve.d.ts +43 -0
- package/dist/daemon/daemon-resolve.js +69 -0
- package/dist/daemon/dev-servers.d.ts +15 -0
- package/dist/daemon/dev-servers.js +68 -0
- package/dist/daemon/wake-decision.d.ts +32 -0
- package/dist/daemon/wake-decision.js +38 -0
- package/dist/domain/domain-tools.js +2 -2
- package/dist/events/contradictions.d.ts +18 -0
- package/dist/events/contradictions.js +134 -21
- package/dist/events/declared.d.ts +7 -4
- package/dist/events/declared.js +15 -6
- package/dist/events/echo-mismatch.js +64 -14
- package/dist/events/event-filters.d.ts +8 -0
- package/dist/events/event-filters.js +19 -2
- package/dist/events/predicate-eval.js +53 -5
- 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.d.ts +5 -0
- package/dist/events/predicate.js +37 -5
- package/dist/events/split-text-miss.d.ts +30 -0
- package/dist/events/split-text-miss.js +44 -0
- package/dist/flows/flake.d.ts +6 -6
- 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 +239 -22
- 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-tools.js +58 -32
- package/dist/flows/flows.d.ts +8 -0
- package/dist/flows/flows.js +468 -2
- 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 +30 -12
- 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/verify-change-tools.js +5 -2
- package/dist/honesty/blind-spots.d.ts +55 -2
- package/dist/honesty/blind-spots.js +63 -3
- package/dist/honesty/honesty.d.ts +24 -0
- package/dist/honesty/honesty.js +25 -1
- package/dist/honesty/instrumentation-gaps.d.ts +9 -0
- package/dist/honesty/instrumentation-gaps.js +29 -2
- package/dist/honesty/verified.d.ts +2 -1
- package/dist/honesty/verified.js +16 -1
- package/dist/http-server.js +12 -0
- package/dist/impact/impact-recorder.d.ts +31 -8
- package/dist/impact/impact-recorder.js +104 -21
- package/dist/impact/impact-store.d.ts +15 -1
- package/dist/impact/impact-store.js +43 -2
- package/dist/index.js +113 -7
- package/dist/init/agent-rules.d.ts +13 -6
- package/dist/init/agent-rules.js +23 -12
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +28 -0
- package/dist/init/capabilities.d.ts +4 -1
- package/dist/init/capabilities.js +22 -2
- package/dist/init/confirm.d.ts +31 -7
- package/dist/init/confirm.js +48 -8
- package/dist/init/cra.d.ts +20 -4
- package/dist/init/cra.js +57 -17
- 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 +3 -1
- package/dist/init/dev-script.js +8 -5
- 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 +1 -1
- package/dist/init/mcp.js +1 -1
- package/dist/init/node-io.js +11 -1
- package/dist/init/plan-framework.js +81 -45
- package/dist/init/plan.d.ts +20 -7
- package/dist/init/plan.js +20 -45
- package/dist/init/preflight.d.ts +28 -0
- package/dist/init/preflight.js +38 -0
- package/dist/init/run.d.ts +32 -9
- package/dist/init/run.js +141 -119
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/snippets.js +41 -25
- 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.js +25 -5
- package/dist/input/scroll-tools.js +4 -6
- package/dist/input/viewport-tools.js +5 -4
- package/dist/intent/inline-intent.d.ts +10 -19
- package/dist/intent/inline-intent.js +91 -1
- 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 +12 -0
- package/dist/intent/intent-store.js +21 -0
- package/dist/intent/intent-subject.d.ts +45 -0
- package/dist/intent/intent-subject.js +177 -0
- package/dist/intent/intent-tools.js +65 -2
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +15 -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 +18 -2
- package/dist/mcp/attach-memory.js +50 -7
- 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 -1
- package/dist/mcp/mcp-proxy.js +52 -62
- package/dist/mcp/mcp.js +7 -1
- 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/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +12 -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/propose-consequences.js +12 -3
- package/dist/pool/browser-pool.d.ts +63 -4
- package/dist/pool/browser-pool.js +113 -4
- package/dist/pool/playwright-launcher.js +9 -0
- package/dist/project/project-tools.js +3 -3
- package/dist/project/reticle-dir.d.ts +15 -2
- package/dist/project/reticle-dir.js +21 -4
- 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 +22 -1
- 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/lease-visibility.d.ts +67 -0
- package/dist/session/lease-visibility.js +76 -0
- package/dist/session/no-session-diagnosis.d.ts +23 -0
- package/dist/session/no-session-diagnosis.js +58 -17
- package/dist/session/no-session-next-action.d.ts +7 -0
- package/dist/session/no-session-next-action.js +42 -3
- package/dist/session/no-session-watch.d.ts +10 -6
- package/dist/session/no-session-watch.js +51 -19
- 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-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 +61 -3
- 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.js +36 -1
- package/dist/session/session-tools.js +2 -2
- package/dist/session/session.d.ts +22 -2
- package/dist/session/session.js +87 -7
- 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/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.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 -1
- package/dist/telemetry/project-profile.js +11 -6
- package/dist/telemetry/proxy-telemetry.d.ts +3 -0
- package/dist/telemetry/proxy-telemetry.js +21 -0
- 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/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 +100 -213
- package/dist/tools/assert-verdict.js +22 -16
- package/dist/tools/browser-tools.js +6 -2
- package/dist/tools/dynamic-tools.js +24 -5
- package/dist/tools/error-recovery.d.ts +1 -0
- package/dist/tools/error-recovery.js +8 -0
- package/dist/tools/invoke-tool.js +59 -9
- package/dist/tools/lease-tools.d.ts +30 -4
- package/dist/tools/lease-tools.js +169 -5
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +22 -0
- package/dist/tools/numeric-bounds.d.ts +100 -0
- package/dist/tools/numeric-bounds.js +125 -0
- package/dist/tools/observe-tools.js +47 -40
- package/dist/tools/read-tools.js +28 -7
- package/dist/tools/real-input-attempt.d.ts +10 -0
- package/dist/tools/real-input-attempt.js +59 -1
- package/dist/tools/reconcile-tools.js +4 -4
- package/dist/tools/tool-kit.d.ts +12 -0
- package/dist/tools/tool-kit.js +6 -1
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- 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 +56 -8
- 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 +18 -0
- package/dist/update/update-nudge.js +20 -0
- 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/cli/doctor.mdx +1 -0
- package/docs/cli/serve.mdx +4 -2
- package/docs/cli.mdx +7 -3
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +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.mdx +1 -1
- package/docs/skill-file.mdx +3 -3
- package/docs/telemetry-contract.md +108 -4
- package/docs/tools-act-sequence.mdx +16 -0
- package/docs/usage.md +1 -1
- package/package.json +4 -3
- package/dist/init/cursor.d.ts +0 -24
- package/dist/init/cursor.js +0 -80
package/dist/mcp/mcp-proxy.js
CHANGED
|
@@ -1,7 +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 { isToolsListRequest, ToolCatalogCache } from './tool-catalog-cache.js';
|
|
4
|
-
import { rememberEnumerated, rememberProxyStarted } from './attach-memory.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';
|
|
5
5
|
import { toolsChangedNotification } from './proxy-handshake.js';
|
|
6
6
|
import { LOOPBACK_HOST, MCP_SSE_PATH, MCP_SHUTDOWN_EVENT, ReticleEnv, CONTRACT_FINGERPRINT, } from '@reticlehq/core';
|
|
7
7
|
import { SseFrameParser } from './sse-frame-parser.js';
|
|
@@ -16,6 +16,7 @@ import { readProjectId } from '../cli/cli-port.js';
|
|
|
16
16
|
import { PEER_VERSION_PARAM, PEER_CONTRACT_PARAM } from '../version/peer-announce.js';
|
|
17
17
|
import { onStreamDrop, onClientRequest, onReconnectBudgetSpent, onQueueExpired, OnDrop, OnRequest, } from './proxy-lifecycle.js';
|
|
18
18
|
import { describePresence, probePresence } from '../daemon/port-presence.js';
|
|
19
|
+
import { flushProxySessionMetrics } from '../telemetry/proxy-telemetry.js';
|
|
19
20
|
/**
|
|
20
21
|
* The same `/status` probe `doctor`, `status` and `kill` ask with. Reused rather than re-written:
|
|
21
22
|
* the whole defect this import closes was the proxy answering a DIFFERENT question from every other
|
|
@@ -28,11 +29,11 @@ import { fetchStatus as fetchDaemonStatus } from '../cli/cli-launch.js';
|
|
|
28
29
|
*/
|
|
29
30
|
export { accountProxyLogWrite, proxyLog, proxyLogPath, PROXY_LOG_CHECK_BYTES, recoverOversizedProxyLog, setProxyLogPort, } from './proxy-log.js';
|
|
30
31
|
import { proxyLog } from './proxy-log.js';
|
|
31
|
-
import { log } from '../log.js';
|
|
32
32
|
import { OutageReason, OutageStage, reportMcpOutage } from './mcp-outage.js';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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';
|
|
36
37
|
/**
|
|
37
38
|
* How many consecutive failed reconnects before the proxy stops RETRYING. It does not stop serving:
|
|
38
39
|
* the budget ends the retry loop and the proxy goes dormant, where the handshake and `tools/list`
|
|
@@ -51,9 +52,6 @@ export const MAX_RECONNECT_ATTEMPTS = (() => {
|
|
|
51
52
|
const raw = Number(process.env[ReticleEnv.RECONNECT_ATTEMPTS]);
|
|
52
53
|
return Number.isInteger(raw) && raw > 0 ? raw : DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
53
54
|
})();
|
|
54
|
-
export function reconnectDelayMs(attempt) {
|
|
55
|
-
return Math.min(RECONNECT_BASE_MS * attempt, RECONNECT_CAP_MS);
|
|
56
|
-
}
|
|
57
55
|
/** JSON-RPC id the proxy uses for its own replayed `initialize` — never one the client could send. */
|
|
58
56
|
export const RECONNECT_INITIALIZE_ID = '__reticle_proxy_reinit';
|
|
59
57
|
function parseJsonRpc(line) {
|
|
@@ -254,49 +252,6 @@ export class HandshakeReplay {
|
|
|
254
252
|
return true;
|
|
255
253
|
}
|
|
256
254
|
}
|
|
257
|
-
/**
|
|
258
|
-
* POST one JSON-RPC line into the daemon's session. Resolves null on success, or a short reason
|
|
259
|
-
* when the request never reached a server that will answer it.
|
|
260
|
-
*
|
|
261
|
-
* It used to resolve `void` in every case, logging the failure and moving on. That is the THIRD way
|
|
262
|
-
* a call goes unanswered, and the only one neither `streamLossReplies` nor the queue timer can see:
|
|
263
|
-
* the SSE stream is healthy (so nothing drops) and the request was forwarded (so nothing is queued),
|
|
264
|
-
* but the POST leg is its own TCP connection and an ECONNRESET on it means the daemon never received
|
|
265
|
-
* the call. Nobody was ever going to reply, and the caller waited for its own timeout.
|
|
266
|
-
*/
|
|
267
|
-
function postToSession(url, body) {
|
|
268
|
-
return new Promise((resolve) => {
|
|
269
|
-
const parsed = new URL(url);
|
|
270
|
-
const bodyBuf = Buffer.from(body, 'utf8');
|
|
271
|
-
const options = {
|
|
272
|
-
host: parsed.hostname,
|
|
273
|
-
port: parsed.port !== '' ? parseInt(parsed.port, 10) : 80,
|
|
274
|
-
path: `${parsed.pathname}${parsed.search}`,
|
|
275
|
-
method: 'POST',
|
|
276
|
-
headers: {
|
|
277
|
-
'Content-Type': 'application/json',
|
|
278
|
-
'Content-Length': bodyBuf.byteLength,
|
|
279
|
-
},
|
|
280
|
-
};
|
|
281
|
-
const req = http.request(options, (res) => {
|
|
282
|
-
const status = res.statusCode ?? 0;
|
|
283
|
-
const rejected = status < 200 || status >= 300;
|
|
284
|
-
if (rejected) {
|
|
285
|
-
// A non-2xx from the daemon MCP endpoint used to be swallowed, hanging the JSON-RPC call
|
|
286
|
-
// client-side with no diagnostic. It is a refusal: no response is coming over the stream.
|
|
287
|
-
log('reticle_mcp_proxy_post_non2xx', { status, path: options.path });
|
|
288
|
-
}
|
|
289
|
-
res.resume(); // drain so the socket is reused
|
|
290
|
-
resolve(rejected ? `daemon rejected the call with HTTP ${String(status)}` : null);
|
|
291
|
-
});
|
|
292
|
-
req.on('error', (err) => {
|
|
293
|
-
log('reticle_mcp_proxy_post_error', { error: err.message });
|
|
294
|
-
resolve(`post failed: ${err.message}`);
|
|
295
|
-
});
|
|
296
|
-
req.write(bodyBuf);
|
|
297
|
-
req.end();
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
255
|
/**
|
|
301
256
|
* Turn an `endpoint` frame's data into a POST target, or null when there is nothing usable in it.
|
|
302
257
|
*
|
|
@@ -439,6 +394,17 @@ ensureDaemon) {
|
|
|
439
394
|
let daemonRetiring = false;
|
|
440
395
|
/** Why the stream we are currently trying to replace went away — carried onto the recovery. */
|
|
441
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;
|
|
442
408
|
/**
|
|
443
409
|
* When the proxy started, and how many times the first connect has been refused since.
|
|
444
410
|
*
|
|
@@ -461,13 +427,20 @@ ensureDaemon) {
|
|
|
461
427
|
const msg = parseJsonRpc(line);
|
|
462
428
|
if (null === msg || msg.id === undefined || !pending.take(msg.id))
|
|
463
429
|
return;
|
|
430
|
+
if (failure.transport) {
|
|
431
|
+
reportMcpOutage(OutageStage.FIRST, {
|
|
432
|
+
reason: OutageReason.CONNECT_ERROR,
|
|
433
|
+
attempts: failure.attempts,
|
|
434
|
+
pendingLost: 1,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
464
437
|
proxyLog('reticle_mcp_proxy_post_unanswered', {
|
|
465
438
|
port,
|
|
466
439
|
method: String(msg.method),
|
|
467
|
-
reason: failure,
|
|
440
|
+
reason: failure.reason,
|
|
468
441
|
note: 'the stream is still up, so nothing else would ever have answered this call',
|
|
469
442
|
});
|
|
470
|
-
emit(transportLossReply(msg.id, failure));
|
|
443
|
+
emit(transportLossReply(msg.id, failure.reason));
|
|
471
444
|
});
|
|
472
445
|
};
|
|
473
446
|
function onSseEvent(event, data, p) {
|
|
@@ -487,9 +460,11 @@ ensureDaemon) {
|
|
|
487
460
|
// off and never reaching the budget that puts the proxy dormant. Measured: 32 retries in 8s.
|
|
488
461
|
// Report the recovery BEFORE the counter is cleared: `attempts` is what coming back actually
|
|
489
462
|
// cost, and it is the only number that can falsify the drop event. See OutageStage.RECOVERED.
|
|
490
|
-
|
|
491
|
-
|
|
463
|
+
const cost = attempts + attemptsBeforeDormant;
|
|
464
|
+
if (cost > 0)
|
|
465
|
+
reportMcpOutage(OutageStage.RECOVERED, { reason: lastDropReason, attempts: cost });
|
|
492
466
|
attempts = 0;
|
|
467
|
+
attemptsBeforeDormant = 0;
|
|
493
468
|
// The new session's McpServer has never seen the client's initialize — replay it first, then
|
|
494
469
|
// flush whatever the client sent while we were reconnecting.
|
|
495
470
|
for (const line of replay.replayLines())
|
|
@@ -710,10 +685,10 @@ ensureDaemon) {
|
|
|
710
685
|
const armLocalHandshake = (line) => {
|
|
711
686
|
if (handshakeAnswered)
|
|
712
687
|
return;
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
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, ''))
|
|
717
692
|
return;
|
|
718
693
|
// Take the debt NOW, not when the timer fires.
|
|
719
694
|
//
|
|
@@ -732,6 +707,13 @@ ensureDaemon) {
|
|
|
732
707
|
deliverHandshakeLocally = (reason) => {
|
|
733
708
|
if (handshakeAnswered || postUrl !== null)
|
|
734
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;
|
|
735
717
|
handshakeAnswered = true;
|
|
736
718
|
deliverHandshakeLocally = undefined;
|
|
737
719
|
// The catalog we are about to serve is ours, not the daemon's, so it has to be corrected
|
|
@@ -779,6 +761,11 @@ ensureDaemon) {
|
|
|
779
761
|
// that never asks look identical to one that asked and was refused.
|
|
780
762
|
if (isToolsListRequest(trimmed))
|
|
781
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);
|
|
782
769
|
const action = onClientRequest(postUrl !== null, dormant);
|
|
783
770
|
if (action === OnRequest.SEND && postUrl !== null) {
|
|
784
771
|
forward(postUrl, trimmed);
|
|
@@ -805,6 +792,8 @@ ensureDaemon) {
|
|
|
805
792
|
// flushed once the new session's `endpoint` frame arrives.
|
|
806
793
|
if (action === OnRequest.WAKE) {
|
|
807
794
|
dormant = false;
|
|
795
|
+
// Budget reset, accounting preserved. See attemptsBeforeDormant.
|
|
796
|
+
attemptsBeforeDormant += attempts;
|
|
808
797
|
attempts = 0;
|
|
809
798
|
void (ensureDaemon?.() ?? Promise.resolve())
|
|
810
799
|
.then(() => connect(false))
|
|
@@ -832,7 +821,8 @@ ensureDaemon) {
|
|
|
832
821
|
process.stdin.on('end', () => {
|
|
833
822
|
const quit = (code) => {
|
|
834
823
|
stopped = true;
|
|
835
|
-
|
|
824
|
+
// Await: fire-and-forget here is how daemon_stopped never arrived.
|
|
825
|
+
void flushProxySessionMetrics().finally(() => process.exit(code));
|
|
836
826
|
};
|
|
837
827
|
if (0 === pending.unanswered.length) {
|
|
838
828
|
quit(0);
|
package/dist/mcp/mcp.js
CHANGED
|
@@ -11,6 +11,7 @@ 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';
|
|
@@ -514,7 +515,12 @@ tools = TOOLS) {
|
|
|
514
515
|
// Every error the agent hits should answer "what next?", not just "what broke".
|
|
515
516
|
return {
|
|
516
517
|
isError: true,
|
|
517
|
-
content: [
|
|
518
|
+
content: [
|
|
519
|
+
{
|
|
520
|
+
type: 'text',
|
|
521
|
+
text: JSON.stringify(takeVersionSkewOnto(buildErrorPayload(message))),
|
|
522
|
+
},
|
|
523
|
+
],
|
|
518
524
|
};
|
|
519
525
|
}
|
|
520
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
|
*
|
|
@@ -103,6 +103,37 @@ export function toolsChangedNotification(catalogWasLocal) {
|
|
|
103
103
|
return null;
|
|
104
104
|
return JSON.stringify({ jsonrpc: '2.0', method: TOOLS_CHANGED_NOTIFICATION });
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* What a locally-answered handshake must say about itself.
|
|
108
|
+
*
|
|
109
|
+
* The proxy answering `initialize` is right — a hang gives the agent no tools AND no diagnosis — but
|
|
110
|
+
* the response it sent was INDISTINGUISHABLE from a daemon's. Same `serverInfo`, same capabilities,
|
|
111
|
+
* same `instructions`. So the client marked the server installed and enabled, the agent got a tool
|
|
112
|
+
* list that was cached at best and empty at worst, and nothing anywhere connected the two facts.
|
|
113
|
+
*
|
|
114
|
+
* Reported from the field, from two different agent hosts at once: the marketplace showed Reticle
|
|
115
|
+
* installed with the whole tool surface enabled, and the agent asked to verify a flow reported that
|
|
116
|
+
* RETICLE WAS NOT PRESENT ON THE MACHINE — while the daemon was running and a browser session was
|
|
117
|
+
* live. That conclusion is the reasonable reading of an empty surface with no explanation attached,
|
|
118
|
+
* and it is unrecoverable: the proxy's own recovery is "the next client request re-probes the port",
|
|
119
|
+
* and a client holding zero tools never makes another request.
|
|
120
|
+
*
|
|
121
|
+
* `instructions` is the channel because it is the only one that survives an empty catalog. It rides
|
|
122
|
+
* on the initialize result, which SUCCEEDED, and every host we target puts it in front of the model.
|
|
123
|
+
* A tool description cannot reach an agent that was handed no tools.
|
|
124
|
+
*
|
|
125
|
+
* The healthy block is kept underneath rather than replaced: an agent that IS about to get a working
|
|
126
|
+
* surface (the common case — the first call wakes a daemon) still needs to know how to drive it.
|
|
127
|
+
*/
|
|
128
|
+
export function degradedInstructions(healthy, port, reason) {
|
|
129
|
+
return `DEGRADED SURFACE — READ THIS BEFORE ANY reticle_* CALL: no Reticle daemon answered on port ${String(port)} (${reason}), so this handshake was completed by the Reticle MCP proxy itself, not by Reticle's daemon. Any tool list you were handed came from a cache or is empty — it is not evidence that anything is behind it.
|
|
130
|
+
|
|
131
|
+
Reticle IS installed: this process is Reticle. Do NOT report it as missing, not present, or not installed — that is the wrong diagnosis and it is the one this state reliably produces. The right one is that the daemon is not reachable yet.
|
|
132
|
+
|
|
133
|
+
Your next reticle_* call starts a daemon and then either works or fails with the specific reason. If you have NO reticle_* tools at all, say exactly that to the user and tell them to run \`reticle doctor\` in a terminal — it reports what is holding port ${String(port)} and whether a daemon can start at all.
|
|
134
|
+
|
|
135
|
+
${healthy}`;
|
|
136
|
+
}
|
|
106
137
|
/**
|
|
107
138
|
* The largest single JSON-RPC line the proxy will accumulate from a client.
|
|
108
139
|
*
|
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
* status`'s "registered, never enumerated" state — see attach-memory.ts.
|
|
24
24
|
*/
|
|
25
25
|
export declare function isToolsListRequest(line: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Is this line a client CALLING a tool?
|
|
28
|
+
*
|
|
29
|
+
* The one fact that separates a working agent link from a merely-registered one. Listing the tools
|
|
30
|
+
* proves a client read the catalogue; it proves nothing about whether a request can cross to the
|
|
31
|
+
* daemon, and the field reports those two states looking identical from every other angle.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isToolCallRequest(line: string): boolean;
|
|
26
34
|
export declare class ToolCatalogCache {
|
|
27
35
|
#private;
|
|
28
36
|
/** Watch a line coming BACK from the daemon; remember it if it is a tool catalog. */
|
|
@@ -35,6 +35,18 @@ function parse(line) {
|
|
|
35
35
|
export function isToolsListRequest(line) {
|
|
36
36
|
return parse(line)?.method === LIST_METHOD;
|
|
37
37
|
}
|
|
38
|
+
/** The JSON-RPC method a client sends to actually USE a tool, as opposed to listing them. */
|
|
39
|
+
const CALL_METHOD = 'tools/call';
|
|
40
|
+
/**
|
|
41
|
+
* Is this line a client CALLING a tool?
|
|
42
|
+
*
|
|
43
|
+
* The one fact that separates a working agent link from a merely-registered one. Listing the tools
|
|
44
|
+
* proves a client read the catalogue; it proves nothing about whether a request can cross to the
|
|
45
|
+
* daemon, and the field reports those two states looking identical from every other angle.
|
|
46
|
+
*/
|
|
47
|
+
export function isToolCallRequest(line) {
|
|
48
|
+
return parse(line)?.method === CALL_METHOD;
|
|
49
|
+
}
|
|
38
50
|
export class ToolCatalogCache {
|
|
39
51
|
#tools;
|
|
40
52
|
/** Watch a line coming BACK from the daemon; remember it if it is a tool catalog. */
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reticle_memory` — ask the project what it already knows.
|
|
3
|
+
*
|
|
4
|
+
* Shared memory reached three callers before this one: the sync writes it, flow replay consults it
|
|
5
|
+
* automatically, and `reticle memory` prints it at a terminal. The one who could not ask was the
|
|
6
|
+
* agent doing the work — mid-drive, holding a question, with a whole tool surface and nothing on it
|
|
7
|
+
* for "what has this team already established about checkout?".
|
|
8
|
+
*
|
|
9
|
+
* That matters twice over. An agent that can read the corpus stops re-deriving what a colleague
|
|
10
|
+
* proved last week; and because the read is attributed, the coverage map's fetch counts finally
|
|
11
|
+
* measure something — they were zero across an entire real corpus, not because the knowledge was
|
|
12
|
+
* useless but because consulting it was a separate act nobody performed.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { ReticleTool } from '../tools/tool-names.js';
|
|
16
|
+
import { countSchema } from '../tools/numeric-bounds.js';
|
|
17
|
+
import { sessionIdShape } from '../tools/tool-kit.js';
|
|
18
|
+
import { asNumber, asString } from '../tools/tools-helpers.js';
|
|
19
|
+
import { sessionRoot } from '../project/session-root.js';
|
|
20
|
+
import { MemoryUnavailable, readProjectMemory } from './project-memory.js';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
/**
|
|
23
|
+
* Enough to orient on, not enough to flood a context.
|
|
24
|
+
*
|
|
25
|
+
* A busy subject on a real project runs to dozens of statements; an agent asking a question wants
|
|
26
|
+
* the ones that settle it. `total` always rides along, so the cap is never silent.
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_LIMIT = 10;
|
|
29
|
+
/** What to do next, per reason. A refusal that does not say how to fix it is a dead end. */
|
|
30
|
+
const ADVICE = {
|
|
31
|
+
[MemoryUnavailable.NOT_LINKED]: 'this project is not linked to a Reticle workspace, so there is no shared memory to read. Run `npx @reticlehq/server login` in the project to link it.',
|
|
32
|
+
[MemoryUnavailable.DISABLED]: 'this project is linked but memory sync is turned off in .reticle/cloud.json, so nothing is being shared. Enable `sync.memory` to use it.',
|
|
33
|
+
[MemoryUnavailable.UNREACHABLE]: 'the workspace could not be reached, or refused the key. Nothing is wrong with this project — try again, or check the link with `reticle status`.',
|
|
34
|
+
};
|
|
35
|
+
export const MEMORY_TOOLS = [
|
|
36
|
+
{
|
|
37
|
+
name: ReticleTool.MEMORY,
|
|
38
|
+
description: 'Ask what THIS PROJECT already knows — the shared memory its team has built up, written by every engineer who verified something and readable by every agent since. Use it BEFORE re-deriving how a feature is supposed to behave: a colleague may already have proved it, and a statement marked `proved` was established by a real verdict rather than written down. With { subject } it narrows to one area (a flow name, or a route like "checkout"); without, it returns across the project. Each entry carries the flow and the source file it is about, so a claim can be checked. The read is recorded as an agent consultation, which is what the dashboard\'s per-flow fetch counts are built from. Returns { subject, known, total } or { error, reason, advice } when this project is not linked to a workspace.',
|
|
39
|
+
inputSchema: {
|
|
40
|
+
subject: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Narrow to one area — a flow name ("checkout-pay") or a route segment ("issues"). Omit to read across the whole project. Subjects come from the coverage map; an unknown one returns nothing rather than erroring.'),
|
|
44
|
+
limit: countSchema
|
|
45
|
+
.optional()
|
|
46
|
+
.describe(`Most relevant N statements. Defaults to ${String(DEFAULT_LIMIT)}. Proved statements rank first, so a small limit still returns the settled ones. \`total\` always reports the true count.`),
|
|
47
|
+
...sessionIdShape,
|
|
48
|
+
},
|
|
49
|
+
outputSchema: {
|
|
50
|
+
subject: z.string().nullable().optional(),
|
|
51
|
+
known: z.array(z.unknown()).optional(),
|
|
52
|
+
total: z.number().optional(),
|
|
53
|
+
error: z.string().optional(),
|
|
54
|
+
reason: z.string().optional(),
|
|
55
|
+
advice: z.string().optional(),
|
|
56
|
+
},
|
|
57
|
+
handler: async (deps, args) => {
|
|
58
|
+
/*
|
|
59
|
+
* The APP's root, not the daemon's.
|
|
60
|
+
*
|
|
61
|
+
* `deps.reticleRoot` is wherever the daemon was launched, which for a user-scoped MCP
|
|
62
|
+
* registration is almost never the project being driven — so the link file it reads is the
|
|
63
|
+
* wrong one or absent, and every project silently reads as "not attached". The same defect
|
|
64
|
+
* has now been fixed three times in this codebase; routing through sessionRoot is what stops
|
|
65
|
+
* a fourth.
|
|
66
|
+
*/
|
|
67
|
+
const root = sessionRoot(deps, asString(args['sessionId']));
|
|
68
|
+
const subject = asString(args['subject']);
|
|
69
|
+
const result = await readProjectMemory(deps.fs, root, homedir(), process.env, {
|
|
70
|
+
subject,
|
|
71
|
+
limit: asNumber(args['limit']) ?? DEFAULT_LIMIT,
|
|
72
|
+
});
|
|
73
|
+
if (!result.ok)
|
|
74
|
+
return {
|
|
75
|
+
error: 'no shared memory available for this project',
|
|
76
|
+
reason: result.reason,
|
|
77
|
+
advice: ADVICE[result.reason],
|
|
78
|
+
};
|
|
79
|
+
return { subject: result.subject, known: result.known, total: result.total };
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FileSystemPort } from '../project/fs-port.js';
|
|
2
|
+
/** Named so a read from the tool surface is distinguishable from the CLI's and from replay's. */
|
|
3
|
+
export declare const MCP_AGENT_ID = "reticle-mcp";
|
|
4
|
+
/** Why a lookup returned nothing. Each one is a different thing for the agent to do next. */
|
|
5
|
+
export declare const MemoryUnavailable: {
|
|
6
|
+
/** No `.reticle/cloud.json` — this project has never been linked to a workspace. */
|
|
7
|
+
readonly NOT_LINKED: "not-linked";
|
|
8
|
+
/** Linked, but the link says not to sync memory. A setting, not a failure. */
|
|
9
|
+
readonly DISABLED: "memory-sync-disabled";
|
|
10
|
+
/** The server was reached and said no, or could not be reached at all. */
|
|
11
|
+
readonly UNREACHABLE: "unreachable";
|
|
12
|
+
};
|
|
13
|
+
export type MemoryUnavailable = (typeof MemoryUnavailable)[keyof typeof MemoryUnavailable];
|
|
14
|
+
export interface KnownThing {
|
|
15
|
+
statement: string;
|
|
16
|
+
status: string;
|
|
17
|
+
flowName: string | null;
|
|
18
|
+
sourceFile: string | null;
|
|
19
|
+
subject: string;
|
|
20
|
+
}
|
|
21
|
+
export type ProjectMemoryResult = {
|
|
22
|
+
ok: true;
|
|
23
|
+
subject: string | null;
|
|
24
|
+
known: KnownThing[];
|
|
25
|
+
total: number;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
reason: MemoryUnavailable;
|
|
29
|
+
};
|
|
30
|
+
export declare const rankKnown: (things: readonly KnownThing[]) => KnownThing[];
|
|
31
|
+
export declare function readProjectMemory(fs: FileSystemPort, root: string, home: string, env: NodeJS.ProcessEnv, opts: {
|
|
32
|
+
subject?: string | undefined;
|
|
33
|
+
limit: number;
|
|
34
|
+
}): Promise<ProjectMemoryResult>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the PROJECT knows, read on demand.
|
|
3
|
+
*
|
|
4
|
+
* Shared memory reached three places before this one: the sync writes it, flow replay consults it
|
|
5
|
+
* automatically, and `reticle memory` prints it at a terminal. The one caller who could not ask was
|
|
6
|
+
* the agent actually doing the work — mid-drive, holding a question, with 44 tools and none of them
|
|
7
|
+
* "what does this team already know about checkout?". That is the gap this closes, and it is the
|
|
8
|
+
* difference between a corpus and a wiki nobody opens.
|
|
9
|
+
*
|
|
10
|
+
* The read is ATTRIBUTED. It carries an agent header, so the coverage map counts it as a
|
|
11
|
+
* consultation and a manager can see which flows are actually being pulled and by whom — the
|
|
12
|
+
* question the fetch counters exist to answer and could not while nothing was reading.
|
|
13
|
+
*/
|
|
14
|
+
import { cloudFetch } from '../cloud/cloud-sync.js';
|
|
15
|
+
import { resolveProjectCloud } from '../cloud/cloud-config.js';
|
|
16
|
+
/** Named so a read from the tool surface is distinguishable from the CLI's and from replay's. */
|
|
17
|
+
export const MCP_AGENT_ID = 'reticle-mcp';
|
|
18
|
+
/** Why a lookup returned nothing. Each one is a different thing for the agent to do next. */
|
|
19
|
+
export const MemoryUnavailable = {
|
|
20
|
+
/** No `.reticle/cloud.json` — this project has never been linked to a workspace. */
|
|
21
|
+
NOT_LINKED: 'not-linked',
|
|
22
|
+
/** Linked, but the link says not to sync memory. A setting, not a failure. */
|
|
23
|
+
DISABLED: 'memory-sync-disabled',
|
|
24
|
+
/** The server was reached and said no, or could not be reached at all. */
|
|
25
|
+
UNREACHABLE: 'unreachable',
|
|
26
|
+
};
|
|
27
|
+
/** Defensive: the wire is somebody else's server, so every field is checked before it is trusted. */
|
|
28
|
+
const asKnown = (raw) => {
|
|
29
|
+
if (null === raw || typeof raw !== 'object')
|
|
30
|
+
return null;
|
|
31
|
+
const r = raw;
|
|
32
|
+
const statement = r['statement'];
|
|
33
|
+
if (typeof statement !== 'string' || 0 === statement.length)
|
|
34
|
+
return null;
|
|
35
|
+
return {
|
|
36
|
+
statement,
|
|
37
|
+
status: 'string' === typeof r['status'] ? r['status'] : 'unknown',
|
|
38
|
+
flowName: 'string' === typeof r['flowName'] ? r['flowName'] : null,
|
|
39
|
+
sourceFile: 'string' === typeof r['sourceFile'] ? r['sourceFile'] : null,
|
|
40
|
+
subject: 'string' === typeof r['subject'] ? r['subject'] : 'unsorted',
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Proved first, then the rest.
|
|
45
|
+
*
|
|
46
|
+
* An agent reading this is about to act on it, and a statement a verdict has actually established
|
|
47
|
+
* is worth more than one somebody merely wrote down. Stable within each group so the same call
|
|
48
|
+
* twice returns the same order — an unstable list reads as the corpus churning when it has not.
|
|
49
|
+
*/
|
|
50
|
+
const PROVED = 'proved';
|
|
51
|
+
export const rankKnown = (things) => [
|
|
52
|
+
...things.filter((t) => PROVED === t.status),
|
|
53
|
+
...things.filter((t) => PROVED !== t.status),
|
|
54
|
+
];
|
|
55
|
+
export async function readProjectMemory(fs, root, home, env, opts) {
|
|
56
|
+
const cloud = await resolveProjectCloud(fs, root, home, env);
|
|
57
|
+
if (null === cloud.config)
|
|
58
|
+
return { ok: false, reason: MemoryUnavailable.NOT_LINKED };
|
|
59
|
+
if (!cloud.policy.memory)
|
|
60
|
+
return { ok: false, reason: MemoryUnavailable.DISABLED };
|
|
61
|
+
const query = opts.subject === undefined ? '' : `?subject=${encodeURIComponent(opts.subject)}`;
|
|
62
|
+
try {
|
|
63
|
+
const res = await cloudFetch(`${cloud.config.url}/v1/memory${query}`, {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
headers: {
|
|
66
|
+
authorization: `Bearer ${cloud.config.apiKey}`,
|
|
67
|
+
// What makes this read COUNT as an agent consulting the corpus rather than a human
|
|
68
|
+
// browsing — the distinction the dashboard's "times an agent consulted it" rests on.
|
|
69
|
+
'x-reticle-agent': MCP_AGENT_ID,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
if (200 !== res.status)
|
|
73
|
+
return { ok: false, reason: MemoryUnavailable.UNREACHABLE };
|
|
74
|
+
// `cloudFetch` returns a real Response, so the body is a METHOD. Reading `res.json` as a
|
|
75
|
+
// property yields the function, and every downstream field is undefined — a silent "the project
|
|
76
|
+
// knows nothing" that is indistinguishable from the honest empty case. It cost a live drive to
|
|
77
|
+
// find once already.
|
|
78
|
+
const body = (await res.json());
|
|
79
|
+
const entries = body?.entries;
|
|
80
|
+
if (!Array.isArray(entries))
|
|
81
|
+
return { ok: false, reason: MemoryUnavailable.UNREACHABLE };
|
|
82
|
+
const known = entries.map(asKnown).filter((k) => k !== null);
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
subject: opts.subject ?? null,
|
|
86
|
+
// Capped for the agent's context, but `total` always reports the truth so the cap is never
|
|
87
|
+
// silent — a truncated list that looks complete is how an agent concludes a project knows
|
|
88
|
+
// less than it does.
|
|
89
|
+
known: rankKnown(known).slice(0, opts.limit),
|
|
90
|
+
total: known.length,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return { ok: false, reason: MemoryUnavailable.UNREACHABLE };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventType, PredicateKind } from '@reticlehq/core';
|
|
2
|
+
import { routePathOf } from '../events/predicate-route.js';
|
|
2
3
|
const TIER = { SIGNAL: 0, CONSEQUENCE: 1, PRESENCE: 2 };
|
|
3
4
|
function pathname(url) {
|
|
4
5
|
if (typeof url !== 'string')
|
|
@@ -66,10 +67,18 @@ export function proposeConsequences(events) {
|
|
|
66
67
|
break;
|
|
67
68
|
}
|
|
68
69
|
case EventType.ROUTE_CHANGE: {
|
|
69
|
-
const
|
|
70
|
-
if ('string' === typeof
|
|
70
|
+
const docPath = data['pathname'];
|
|
71
|
+
if ('string' === typeof docPath) {
|
|
72
|
+
// `pathname`, not `to`: the evaluator has never accepted `to`, so this surface was handing
|
|
73
|
+
// the agent a predicate reticle_assert refuses with "unknown field to". A proposal that
|
|
74
|
+
// cannot be evaluated is worse than no proposal — see the schema check in the test.
|
|
75
|
+
//
|
|
76
|
+
// And the ROUTER's path, not the document's: under a hash router the document pathname is
|
|
77
|
+
// `/` on every page, so proposing it proposes an assertion that says nothing about where
|
|
78
|
+
// the app went.
|
|
79
|
+
const to = routePathOf(docPath, 'string' === typeof data['hash'] ? data['hash'] : '');
|
|
71
80
|
add(`route:${to}`, {
|
|
72
|
-
predicate: { kind: PredicateKind.ROUTE, to },
|
|
81
|
+
predicate: { kind: PredicateKind.ROUTE, pathname: to },
|
|
73
82
|
tier: TIER.CONSEQUENCE,
|
|
74
83
|
label: `route changes to ${to}`,
|
|
75
84
|
weak: false,
|