@reticlehq/server 3.3.0 → 3.5.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/LICENSE-ENTERPRISE +1 -1
- package/README.md +3 -3
- package/SKILL.md +12 -14
- package/dist/chromium-launch-options.d.ts +4 -3
- package/dist/chromium-launch-options.js +6 -2
- package/dist/command/cli/adhoc-suite.d.ts +22 -0
- package/dist/command/cli/adhoc-suite.js +90 -19
- package/dist/command/cli/adhoc-verdict.d.ts +35 -0
- package/dist/command/cli/adhoc-verdict.js +90 -3
- package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
- package/dist/command/cli/answers/gate-hook-message.js +6 -0
- package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
- package/dist/command/cli/auth/cloud-keystore.js +0 -3
- package/dist/command/cli/auth/cloud-session.d.ts +6 -2
- package/dist/command/cli/auth/cloud-session.js +2 -0
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +7 -5
- package/dist/command/cli/cli-flow-commands.d.ts +3 -6
- package/dist/command/cli/cli-flow-commands.js +67 -4
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +24 -4
- package/dist/command/cli/cli-update-commands.js +8 -1
- package/dist/command/cli/cli-verify.d.ts +24 -3
- package/dist/command/cli/cli-verify.js +54 -11
- package/dist/command/cli/cloud-cli.js +64 -2
- package/dist/command/cli/cloud-kit.js +3 -5
- package/dist/command/cli/cloud-login.d.ts +1 -1
- package/dist/command/cli/cloud-login.js +27 -8
- package/dist/command/cli/demo/demo-page.js +6 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
- package/dist/command/cli/launch/cli-launch.d.ts +8 -0
- package/dist/command/cli/launch/cli-launch.js +21 -9
- package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
- package/dist/command/cli/launch/sdk-version-match.js +206 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
- package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
- package/dist/command/cli/ports/resolve/cli-port.js +125 -15
- package/dist/command/cli/setup-mcp-cli.js +21 -13
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +21 -7
- package/dist/command/cli.js +79 -11
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
- package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
- package/dist/command/daemon/daemon-resolve.d.ts +9 -0
- package/dist/command/daemon/daemon-resolve.js +1 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
- package/dist/command/dev/stale-issue-guard.d.ts +3 -4
- package/dist/command/dev/stale-issue-guard.js +8 -8
- package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
- package/dist/command/setup/bringup/connect-lease.js +38 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
- package/dist/command/setup/bringup/dev-server-files.js +131 -0
- package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
- package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
- package/dist/command/setup/bringup/dev-server-wait.js +1 -1
- package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
- package/dist/command/setup/bringup/ensure-daemon.js +135 -16
- package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
- package/dist/command/setup/bringup/static-page-server.js +151 -0
- package/dist/command/setup/desktop-shape.d.ts +4 -0
- package/dist/command/setup/desktop-shape.js +18 -2
- package/dist/command/setup/init/init-port.d.ts +15 -0
- package/dist/command/setup/init/init-port.js +44 -0
- package/dist/command/setup/init/init-runtime.d.ts +1 -1
- package/dist/command/setup/init/init-runtime.js +91 -9
- package/dist/command/setup/node-effects.d.ts +27 -14
- package/dist/command/setup/node-effects.js +86 -45
- package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
- package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
- package/dist/command/setup/probe/page-probe.d.ts +13 -2
- package/dist/command/setup/probe/page-probe.js +18 -3
- package/dist/command/setup/run-setup.d.ts +28 -1
- package/dist/command/setup/run-setup.js +103 -15
- package/dist/command/setup/session-pick.d.ts +13 -0
- package/dist/command/setup/session-pick.js +20 -0
- package/dist/command/setup/setup-command.d.ts +2 -14
- package/dist/command/setup/setup-command.js +105 -31
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +17 -8
- package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
- package/dist/command/setup/terminal/wait-progress.js +17 -0
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- package/dist/features/crawl/crawl-empty.js +1 -1
- package/dist/features/crawl/crawl-tools.d.ts +6 -0
- package/dist/features/crawl/crawl-tools.js +57 -0
- package/dist/features/crawl/crawl.js +171 -157
- package/dist/features/exhaust/app-coverage.d.ts +33 -0
- package/dist/features/exhaust/app-coverage.js +31 -0
- package/dist/features/exhaust/code-coverage.d.ts +47 -0
- package/dist/features/exhaust/code-coverage.js +77 -0
- package/dist/features/exhaust/exhaust-run.d.ts +33 -0
- package/dist/features/exhaust/exhaust-run.js +82 -0
- package/dist/features/exhaust/explorer.d.ts +103 -0
- package/dist/features/exhaust/explorer.js +195 -0
- package/dist/features/exhaust/failure-branches.d.ts +33 -0
- package/dist/features/exhaust/failure-branches.js +92 -0
- package/dist/features/exhaust/ledger.d.ts +202 -0
- package/dist/features/exhaust/ledger.js +252 -0
- package/dist/features/exhaust/session-fold.d.ts +31 -0
- package/dist/features/exhaust/session-fold.js +79 -0
- package/dist/features/exhaust/session-port.d.ts +36 -0
- package/dist/features/exhaust/session-port.js +102 -0
- package/dist/features/harness/drive-report.d.ts +0 -9
- package/dist/features/harness/drive-report.js +18 -0
- package/dist/features/harness/driver.js +4 -7
- package/dist/features/harness/fill-values.d.ts +10 -0
- package/dist/features/harness/fill-values.js +50 -11
- package/dist/features/harness/jev-driver.js +30 -13
- package/dist/features/harness/openai-driver.d.ts +12 -0
- package/dist/features/harness/openai-driver.js +21 -12
- package/dist/features/harness/platform-config.js +5 -6
- package/dist/features/license/license.d.ts +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +56 -19
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- package/dist/judgement/domain/domain-model.js +3 -1
- package/dist/judgement/oracles/self-instrument.js +4 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
- package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
- package/dist/judgement/runs/verification-sync.d.ts +3 -1
- package/dist/judgement/runs/verification-sync.js +1 -1
- package/dist/language/flows/change/affected.d.ts +9 -0
- package/dist/language/flows/change/affected.js +35 -0
- package/dist/language/flows/change/gate.d.ts +20 -0
- package/dist/language/flows/change/gate.js +12 -1
- package/dist/language/flows/flow-author.d.ts +7 -0
- package/dist/language/flows/flow-author.js +19 -0
- package/dist/language/flows/flow-expect-grammar.js +1 -1
- package/dist/language/flows/flow-journey.d.ts +54 -0
- package/dist/language/flows/flow-journey.js +119 -0
- package/dist/language/flows/flow-replay-run.d.ts +2 -3
- package/dist/language/flows/flow-replay-run.js +13 -47
- package/dist/language/flows/flow-replay.d.ts +22 -8
- package/dist/language/flows/flow-replay.js +55 -9
- package/dist/language/flows/flow-result.d.ts +2 -0
- package/dist/language/flows/flow-result.js +25 -0
- package/dist/language/flows/flow-tools.js +17 -5
- package/dist/language/flows/flows.d.ts +10 -1
- package/dist/language/flows/flows.js +90 -14
- package/dist/language/flows/recording/replay-halt.js +3 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
- package/dist/language/flows/recording/tape/recordings.js +55 -1
- package/dist/language/flows/replay.d.ts +19 -0
- package/dist/language/flows/replay.js +42 -6
- package/dist/language/flows/verify-change-tools.js +5 -2
- package/dist/launch-chromium.d.ts +80 -0
- package/dist/launch-chromium.js +218 -0
- package/dist/memory/cloud/cloud-config.d.ts +11 -2
- package/dist/memory/cloud/cloud-config.js +47 -7
- package/dist/memory/cloud/cloud-sync.d.ts +4 -1
- package/dist/memory/cloud/cloud-sync.js +6 -9
- package/dist/memory/cloud/harness-offer.js +5 -6
- package/dist/memory/cloud/harness-switch.js +5 -6
- package/dist/memory/cloud/sync-cycle.d.ts +77 -3
- package/dist/memory/cloud/sync-cycle.js +325 -55
- package/dist/memory/cloud/sync-daemon.js +24 -20
- package/dist/memory/cloud/sync-disk.js +16 -2
- package/dist/memory/intent/intent-shard.d.ts +10 -32
- package/dist/memory/intent/intent-shard.js +0 -10
- package/dist/memory/intent/intent-tools.js +8 -3
- package/dist/memory/journal/drive-flow.d.ts +18 -13
- package/dist/memory/journal/drive-flow.js +45 -37
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
- package/dist/memory/journal/session-end.d.ts +9 -1
- package/dist/memory/journal/session-end.js +24 -12
- package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
- package/dist/memory/project/artifact-root-resolver.js +46 -19
- package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
- package/dist/memory/project/dir/reticle-dir.js +1 -0
- package/dist/memory/project/project-tools.js +3 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
- package/dist/portal/bridge/auth-failure-reason.js +4 -2
- package/dist/portal/bridge/bridge.d.ts +12 -3
- package/dist/portal/bridge/bridge.js +49 -3
- package/dist/portal/input/js-coverage.d.ts +25 -0
- package/dist/portal/input/js-coverage.js +30 -0
- package/dist/portal/input/real-input.d.ts +11 -0
- package/dist/portal/input/real-input.js +29 -4
- package/dist/portal/input/viewport-tools.js +16 -6
- package/dist/portal/pool/playwright-launcher.d.ts +6 -0
- package/dist/portal/pool/playwright-launcher.js +20 -13
- package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
- package/dist/portal/session/dev-server/dev-command.js +30 -12
- package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
- package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
- package/dist/portal/session/dev-server/route-status-probe.js +57 -0
- package/dist/portal/session/facts/observed-state.d.ts +11 -0
- package/dist/portal/session/facts/observed-state.js +34 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
- package/dist/portal/session/no-session-diagnosis.js +62 -5
- package/dist/portal/session/no-session-next-action.d.ts +13 -0
- package/dist/portal/session/no-session-next-action.js +48 -9
- package/dist/portal/session/no-session-watch.d.ts +18 -0
- package/dist/portal/session/no-session-watch.js +88 -2
- package/dist/portal/session/session-health.js +3 -2
- package/dist/portal/session/session-manager.d.ts +20 -6
- package/dist/portal/session/session-manager.js +19 -3
- package/dist/portal/session/session-reaper.d.ts +28 -0
- package/dist/portal/session/session-reaper.js +49 -1
- package/dist/portal/session/session.d.ts +10 -8
- package/dist/portal/session/session.js +24 -32
- package/dist/surface/http-server.d.ts +3 -2
- package/dist/surface/http-server.js +8 -5
- package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
- package/dist/surface/mcp/mcp-proxy.js +2 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- package/dist/surface/mcp/server-instructions.js +9 -7
- package/dist/surface/tools/act/after-match.d.ts +17 -0
- package/dist/surface/tools/act/after-match.js +30 -0
- package/dist/surface/tools/act/durable.d.ts +69 -0
- package/dist/surface/tools/act/durable.js +139 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
- package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
- package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
- package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
- package/dist/surface/tools/act/proved-controls.d.ts +9 -0
- package/dist/surface/tools/act/proved-controls.js +11 -0
- package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
- package/dist/surface/tools/act/sequence-grade.js +32 -1
- package/dist/surface/tools/act-capsule.d.ts +7 -0
- package/dist/surface/tools/act-capsule.js +46 -1
- package/dist/surface/tools/act-sequence-tool.js +25 -8
- package/dist/surface/tools/act-tools.js +28 -52
- package/dist/surface/tools/assert/assert-grade.js +12 -1
- package/dist/surface/tools/assert/assert-verdict.js +2 -2
- package/dist/surface/tools/browser-tools.js +14 -2
- package/dist/surface/tools/coverage-tools.js +27 -1
- package/dist/surface/tools/dynamic-tools.js +29 -2
- package/dist/surface/tools/error-recovery.js +5 -6
- package/dist/surface/tools/feedback-invite.js +6 -4
- package/dist/surface/tools/harness-explore.d.ts +3 -2
- package/dist/surface/tools/harness-explore.js +12 -7
- package/dist/surface/tools/invoke-tool.js +12 -8
- package/dist/surface/tools/lease-tools.js +15 -6
- package/dist/surface/tools/live-call-text.js +4 -1
- package/dist/surface/tools/observe-tools.js +9 -1
- package/dist/surface/tools/tool-kit.d.ts +10 -0
- package/dist/surface/tools/tools.js +4 -0
- package/dist/telemetry/argument-shape.d.ts +0 -2
- package/dist/telemetry/argument-shape.js +1 -5
- package/dist/telemetry/error-class.d.ts +2 -3
- package/dist/telemetry/error-class.js +2 -3
- package/dist/telemetry/harness-drive.d.ts +0 -2
- package/dist/telemetry/harness-drive.js +0 -4
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- package/dist/wire-journal.d.ts +3 -0
- package/dist/wire-journal.js +4 -0
- package/dist/workspace-packages.d.ts +0 -2
- package/dist/workspace-packages.js +1 -9
- package/docs/README.md +4 -2
- package/docs/actions.mdx +3 -4
- package/docs/agent-cheatsheet.md +4 -4
- package/docs/architecture.md +6 -6
- package/docs/best-practices.mdx +1 -1
- package/docs/capabilities.mdx +32 -1
- package/docs/cli/cloud.mdx +20 -8
- package/docs/cli/doctor.mdx +4 -0
- package/docs/cli/feedback.mdx +2 -2
- package/docs/cli/gate.mdx +9 -4
- package/docs/cli/update.mdx +10 -0
- package/docs/cli.mdx +22 -7
- package/docs/desktop.mdx +14 -10
- package/docs/environment.mdx +87 -0
- package/docs/faq.mdx +18 -16
- package/docs/first-drive.md +5 -13
- package/docs/flows.md +21 -2
- package/docs/frameworks.mdx +29 -10
- package/docs/getting-started.md +15 -5
- package/docs/http-transport.md +6 -3
- package/docs/human-control.md +2 -2
- package/docs/index.mdx +16 -6
- package/docs/install-agentic.mdx +2 -2
- package/docs/install-manual.mdx +1 -1
- package/docs/integration-patterns.md +3 -3
- package/docs/multi-agent-testing.md +1 -1
- package/docs/packages/babel-plugin.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/react.mdx +2 -2
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +4 -2
- package/docs/packages.mdx +9 -9
- package/docs/predicates.mdx +50 -5
- package/docs/quickstart.mdx +12 -10
- package/docs/recipes.mdx +2 -2
- package/docs/state-management.mdx +2 -2
- package/docs/system-map.md +1 -1
- package/docs/telemetry.md +1 -1
- package/docs/token-efficiency.md +3 -3
- package/docs/tools/act-and-wait.mdx +1 -0
- package/docs/tools/act-sequence.mdx +1 -1
- package/docs/tools/act.mdx +1 -1
- package/docs/tools/console.mdx +1 -1
- package/docs/tools/inspect.mdx +1 -1
- package/docs/tools/network.mdx +1 -1
- package/docs/tools/query.mdx +1 -1
- package/docs/tools/sessions.mdx +1 -1
- package/docs/tools/snapshot.mdx +1 -1
- package/docs/tools/state.mdx +1 -1
- package/docs/tools/wait-for.mdx +1 -1
- package/docs/usage.md +7 -7
- package/docs/use-cases.mdx +157 -0
- package/docs/vs-screenshots.mdx +3 -3
- package/docs/what-is-recorded.md +2 -2
- package/docs/what-is-reticle.mdx +17 -20
- package/docs/why-reticle.mdx +14 -21
- package/package.json +10 -9
- package/dist/judgement/capsule/minimize.d.ts +0 -12
- package/dist/judgement/capsule/minimize.js +0 -22
- package/dist/judgement/phenomena/phenomena.d.ts +0 -27
- package/dist/judgement/phenomena/phenomena.js +0 -101
- package/dist/language/flows/flow-report.d.ts +0 -22
- package/dist/language/flows/flow-report.js +0 -106
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
* It only probes while NOTHING is connected. Once a session is live the question is moot, and a
|
|
10
10
|
* daemon that outlives the agent by hours has no business scanning ports it does not need.
|
|
11
11
|
*/
|
|
12
|
+
import { probeRouteStatus } from './dev-server/route-status-probe.js';
|
|
12
13
|
import { probeDevServers, probeDevServerStates } from './dev-server/dev-server-probe.js';
|
|
13
14
|
import { homedir } from 'node:os';
|
|
14
15
|
import { registeredElsewhere } from '../../memory/recall/registered-projects.js';
|
|
15
16
|
import { explainNoSession } from './no-session-diagnosis.js';
|
|
16
17
|
import { detectDevCommand } from './dev-server/dev-command.js';
|
|
17
18
|
import { nextActionFor, renderNextAction } from './no-session-next-action.js';
|
|
18
|
-
import { readProjectFramework, readProjectId, readProjectPort, } from '../../command/cli/ports/resolve/cli-port.js';
|
|
19
|
+
import { readProjectFramework, readProjectId, readProjectIsDesktop, readProjectPort, } from '../../command/cli/ports/resolve/cli-port.js';
|
|
19
20
|
import { discoverProjectConfigs } from '../../command/cli/config/config-discovery.js';
|
|
20
21
|
import { hasProjectConnectedBefore, rememberConnected, } from '../../memory/recall/prior/connection-memory.js';
|
|
21
22
|
import { isAlive, reticleStateHome } from '../../command/daemon/daemon.js';
|
|
@@ -24,6 +25,8 @@ import { stallUptime } from './stall-clock.js';
|
|
|
24
25
|
import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
|
|
25
26
|
import { findOccupiedSiblings } from '../../command/cli/ports/sibling-ports.js';
|
|
26
27
|
import { isAuthRefusalReason } from '../bridge/auth-failure-reason.js';
|
|
28
|
+
import { devServersForProject } from '@reticlehq/core';
|
|
29
|
+
import { readDevServers } from '../../command/daemon/dev-servers.js';
|
|
27
30
|
/** Slow enough to be free, fast enough that a dev server started 15s ago is already reflected. */
|
|
28
31
|
const REFRESH_MS = 15_000;
|
|
29
32
|
/** The URL an auto-attach opens. `localhost` for the same reason the probe uses it: both stacks. */
|
|
@@ -63,6 +66,15 @@ export function startNoSessionWatch(options) {
|
|
|
63
66
|
*/
|
|
64
67
|
let slowListeners = [];
|
|
65
68
|
let siblingListeners = [];
|
|
69
|
+
/**
|
|
70
|
+
* What the last-known route answered, cached beside the port scan for the same reason the scan
|
|
71
|
+
* is cached: the hint is read synchronously, so every fact it carries has to be gathered before
|
|
72
|
+
* it is asked. Keyed by URL so a status is never attributed to a route other than the one it
|
|
73
|
+
* came from — a fresh departure on a different URL reads as "not asked yet", not as the old
|
|
74
|
+
* answer.
|
|
75
|
+
*/
|
|
76
|
+
let lastKnownStatus;
|
|
77
|
+
const routeStatus = options.routeStatus ?? probeRouteStatus;
|
|
66
78
|
let running = false;
|
|
67
79
|
/** Ports auto-attach has already spent its one attempt on. Bounded: never a loop, never a retry. */
|
|
68
80
|
const attempted = new Set();
|
|
@@ -155,6 +167,31 @@ export function startNoSessionWatch(options) {
|
|
|
155
167
|
* Bounded by construction: one attempt per port per daemon. A failure is recorded, never retried —
|
|
156
168
|
* a retry loop against a broken Chromium install would spin for the daemon's whole life.
|
|
157
169
|
*/
|
|
170
|
+
const ownDevServerPorts = options.ownDevServerPorts ??
|
|
171
|
+
(() => devServersForProject(readDevServers(stateDir), {
|
|
172
|
+
projectId: readProjectId(directory),
|
|
173
|
+
root: directory,
|
|
174
|
+
}).map((entry) => entry.port));
|
|
175
|
+
/**
|
|
176
|
+
* Is `port` demonstrably this project's app? Either its dev server announced it, or this
|
|
177
|
+
* project's own last session was on it. Anything else is a guess, and a guess that opens a
|
|
178
|
+
* browser is worse than the sentence the diagnosis already writes about the ports it saw.
|
|
179
|
+
*/
|
|
180
|
+
const attributable = (port) => {
|
|
181
|
+
if (ownDevServerPorts().includes(port))
|
|
182
|
+
return true;
|
|
183
|
+
const known = options.sessions.lastKnown?.();
|
|
184
|
+
const projectId = readProjectId(directory);
|
|
185
|
+
if (known === undefined || projectId === undefined || known.projectId !== projectId) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
return Number(new URL(known.url).port) === port;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
158
195
|
const autoAttach = async (ports) => {
|
|
159
196
|
const attach = options.attach;
|
|
160
197
|
if (attach === undefined)
|
|
@@ -166,6 +203,8 @@ export function startNoSessionWatch(options) {
|
|
|
166
203
|
return;
|
|
167
204
|
if (!isWired())
|
|
168
205
|
return;
|
|
206
|
+
if (!attributable(only))
|
|
207
|
+
return;
|
|
169
208
|
attempted.add(only);
|
|
170
209
|
try {
|
|
171
210
|
await attach(`${LOCALHOST}:${String(only)}`);
|
|
@@ -193,6 +232,14 @@ export function startNoSessionWatch(options) {
|
|
|
193
232
|
: options.probe === undefined
|
|
194
233
|
? await findOccupiedSiblings(options.port, probeDaemon)
|
|
195
234
|
: [];
|
|
235
|
+
// Asked once per departed URL, on the same background cadence as the port scan, and never
|
|
236
|
+
// on the hint's own path. A url already answered is not asked again; a new departure is.
|
|
237
|
+
const known = options.sessions.lastKnown?.();
|
|
238
|
+
if (known !== undefined && lastKnownStatus?.url !== known.url) {
|
|
239
|
+
const status = await routeStatus(known.url);
|
|
240
|
+
if (status !== undefined)
|
|
241
|
+
lastKnownStatus = { url: known.url, status };
|
|
242
|
+
}
|
|
196
243
|
await autoAttach(ports);
|
|
197
244
|
})
|
|
198
245
|
.catch(() => {
|
|
@@ -225,6 +272,21 @@ export function startNoSessionWatch(options) {
|
|
|
225
272
|
*/
|
|
226
273
|
const lastCloseWasAuthFailure = () => isAuthRefusalReason(options.sessions.lastClosure?.()?.reason) &&
|
|
227
274
|
true !== options.sessions.connectedSinceLastClosure?.();
|
|
275
|
+
/**
|
|
276
|
+
* A hello refused for anything but its token, while it is still the bridge's current state.
|
|
277
|
+
*
|
|
278
|
+
* Only closures that carry the refused page count: the bridge attaches one exactly when a HELLO
|
|
279
|
+
* was turned away, so an origin or handshake-pool refusal (which has its own sentence on
|
|
280
|
+
* `resolve`) is not mistaken for one. Same ordering clause as the token refusal above.
|
|
281
|
+
*/
|
|
282
|
+
const currentHelloRefusal = () => {
|
|
283
|
+
const closure = options.sessions.lastClosure?.();
|
|
284
|
+
if (closure?.page === undefined || isAuthRefusalReason(closure.reason))
|
|
285
|
+
return undefined;
|
|
286
|
+
if (true === options.sessions.connectedSinceLastClosure?.())
|
|
287
|
+
return undefined;
|
|
288
|
+
return { reason: closure.reason, ...closure.page };
|
|
289
|
+
};
|
|
228
290
|
const nextAction = (scope) => {
|
|
229
291
|
const split = splitBrain();
|
|
230
292
|
return nextActionFor({
|
|
@@ -235,8 +297,18 @@ export function startNoSessionWatch(options) {
|
|
|
235
297
|
// Read when asked, like every other fact here: a page can dial at any moment, and a daemon
|
|
236
298
|
// that cached "nothing has been refused" at boot would keep saying so.
|
|
237
299
|
authRefused: lastCloseWasAuthFailure(),
|
|
300
|
+
...(() => {
|
|
301
|
+
const refused = currentHelloRefusal();
|
|
302
|
+
return refused === undefined ? {} : { helloRefused: refused.reason };
|
|
303
|
+
})(),
|
|
238
304
|
...(split === undefined ? {} : { splitBrain: split }),
|
|
239
305
|
listening,
|
|
306
|
+
// The url the departed tab was on — the same tombstone the prose diagnosis quotes, so the
|
|
307
|
+
// command and the sentence beside it name the same page.
|
|
308
|
+
...(() => {
|
|
309
|
+
const known = options.sessions.lastKnown?.();
|
|
310
|
+
return known === undefined ? {} : { lastKnownUrl: known.url };
|
|
311
|
+
})(),
|
|
240
312
|
// Read when asked, like everything else here: a `package.json` can gain a dev script, and a
|
|
241
313
|
// daemon that cached "there is none" at boot would keep saying so for the rest of the day.
|
|
242
314
|
dev: detectDevCommand(directory),
|
|
@@ -272,12 +344,19 @@ export function startNoSessionWatch(options) {
|
|
|
272
344
|
// beside it still said the tab had been closed. One fact, two answers, and only one of them
|
|
273
345
|
// was right. Read when asked, like the rest: a page can be refused at any moment.
|
|
274
346
|
authRefused: lastCloseWasAuthFailure(),
|
|
347
|
+
...(() => {
|
|
348
|
+
const refused = currentHelloRefusal();
|
|
349
|
+
return refused === undefined ? {} : { helloRefused: refused };
|
|
350
|
+
})(),
|
|
275
351
|
// Ranks the causes. Read when asked, like the rest: `init` writes this file after the daemon
|
|
276
352
|
// starts on an ordinary first install.
|
|
277
353
|
...(() => {
|
|
278
354
|
const framework = readProjectFramework(directory);
|
|
279
355
|
return framework === undefined ? {} : { framework };
|
|
280
356
|
})(),
|
|
357
|
+
// A desktop app is opened by its own dev command, never by a browser URL or a lease. Read when
|
|
358
|
+
// asked, like the framework: `init` may wire the app after this daemon started.
|
|
359
|
+
...(readProjectIsDesktop(directory) ? { desktop: true } : {}),
|
|
281
360
|
// Decided from the session that actually went away, not from a lifetime tally: the lease
|
|
282
361
|
// sentence is only right when the thing that vanished WAS a lease.
|
|
283
362
|
leaseExpired: (() => {
|
|
@@ -289,7 +368,14 @@ export function startNoSessionWatch(options) {
|
|
|
289
368
|
// has none to name.
|
|
290
369
|
...(() => {
|
|
291
370
|
const known = options.sessions.lastKnown?.();
|
|
292
|
-
|
|
371
|
+
if (undefined === known)
|
|
372
|
+
return {};
|
|
373
|
+
// The status rides only when it was fetched for THIS url. A departure since the fetch
|
|
374
|
+
// means the cached answer is about a route nobody is asking about.
|
|
375
|
+
const status = lastKnownStatus !== undefined && lastKnownStatus.url === known.url
|
|
376
|
+
? { lastKnownStatus: lastKnownStatus.status }
|
|
377
|
+
: {};
|
|
378
|
+
return { lastKnownUrl: known.url, ...status };
|
|
293
379
|
})(),
|
|
294
380
|
// How long this daemon has been waiting with no app. The diagnosis uses it to surface
|
|
295
381
|
// "install never finished" — the same condition telemetry already knows about.
|
|
@@ -29,7 +29,8 @@ export const PENDING_NAVIGATION_NOTICE_MS = 10_000;
|
|
|
29
29
|
*/
|
|
30
30
|
const SKEW_VERDICT_WARNING = 'version skew: `dispatched` and `settled` on this session mean the event was sent, NOT that the ' +
|
|
31
31
|
'app acted on it — a skewed pair drops actions silently. Re-read the control (aria-checked, ' +
|
|
32
|
-
'data-state, its text) before trusting any verdict from this session, and converge the versions
|
|
32
|
+
'data-state, its text) before trusting any verdict from this session, and converge the versions ' +
|
|
33
|
+
'(see session.versionSkew).';
|
|
33
34
|
/**
|
|
34
35
|
* The age of the oldest request that STARTED and never completed, or undefined when there is none
|
|
35
36
|
* worth reporting.
|
|
@@ -100,7 +101,7 @@ export function healthEnvelope(session) {
|
|
|
100
101
|
// a skewed link makes the ACTION unreliable, and there is no point warning about the quality of an
|
|
101
102
|
// observation of something that may never have happened.
|
|
102
103
|
if (health.versionSkew !== undefined) {
|
|
103
|
-
return { session: health, warning:
|
|
104
|
+
return { session: health, warning: SKEW_VERDICT_WARNING };
|
|
104
105
|
}
|
|
105
106
|
return health.throttled ? { session: health, warning: THROTTLED_WARNING } : { session: health };
|
|
106
107
|
}
|
|
@@ -14,6 +14,17 @@ interface ResolveScope {
|
|
|
14
14
|
}
|
|
15
15
|
/** The scheme://host:port of a URL, or undefined if it can't be parsed. Used to compare origins. */
|
|
16
16
|
export declare function originOf(url: string | undefined): string | undefined;
|
|
17
|
+
/** What a refused hello said about itself: the page it was on and the project it claimed. */
|
|
18
|
+
export interface RefusedPage {
|
|
19
|
+
readonly url?: string;
|
|
20
|
+
readonly projectId?: string;
|
|
21
|
+
}
|
|
22
|
+
/** One bridge-initiated close, with the page that was turned away when the hello named one. */
|
|
23
|
+
export interface ClosureRecord {
|
|
24
|
+
readonly at: number;
|
|
25
|
+
readonly reason: string;
|
|
26
|
+
readonly page?: RefusedPage;
|
|
27
|
+
}
|
|
17
28
|
export declare class SessionManager {
|
|
18
29
|
#private;
|
|
19
30
|
/** Set the active project's default resolve scope (called once at daemon wiring). */
|
|
@@ -57,13 +68,16 @@ export declare class SessionManager {
|
|
|
57
68
|
setNoSessionNextAction(next: (() => NoSessionNextAction | undefined) | undefined): void;
|
|
58
69
|
noSessionNextAction(): NoSessionNextAction | undefined;
|
|
59
70
|
everConnected(): boolean;
|
|
60
|
-
/**
|
|
61
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Record a bridge-initiated close so `resolve` can explain a session that vanished.
|
|
73
|
+
*
|
|
74
|
+
* `page` is what the refused hello said about itself, when it said anything. A refusal is the one
|
|
75
|
+
* piece of positive evidence the no-session diagnosis has, and "a page was refused" without WHICH
|
|
76
|
+
* page leaves the reader unable to tell their app from another project's tab on the same port.
|
|
77
|
+
*/
|
|
78
|
+
noteClosure(reason: string, at: number, page?: RefusedPage): void;
|
|
62
79
|
/** The most recent bridge-initiated close, if any. */
|
|
63
|
-
lastClosure():
|
|
64
|
-
at: number;
|
|
65
|
-
reason: string;
|
|
66
|
-
} | undefined;
|
|
80
|
+
lastClosure(): ClosureRecord | undefined;
|
|
67
81
|
connectedSinceLastClosure(): boolean;
|
|
68
82
|
/**
|
|
69
83
|
* The id of the session that most recently went away, if any.
|
|
@@ -325,9 +325,15 @@ export class SessionManager {
|
|
|
325
325
|
everConnected() {
|
|
326
326
|
return this.#everConnected;
|
|
327
327
|
}
|
|
328
|
-
/**
|
|
329
|
-
|
|
330
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Record a bridge-initiated close so `resolve` can explain a session that vanished.
|
|
330
|
+
*
|
|
331
|
+
* `page` is what the refused hello said about itself, when it said anything. A refusal is the one
|
|
332
|
+
* piece of positive evidence the no-session diagnosis has, and "a page was refused" without WHICH
|
|
333
|
+
* page leaves the reader unable to tell their app from another project's tab on the same port.
|
|
334
|
+
*/
|
|
335
|
+
noteClosure(reason, at, page) {
|
|
336
|
+
this.#recentClosures.push({ at, reason, ...(page === undefined ? {} : { page }) });
|
|
331
337
|
if (this.#recentClosures.length > MAX_REMEMBERED_CLOSURES)
|
|
332
338
|
this.#recentClosures.shift();
|
|
333
339
|
this.#connectedSinceLastClosure = false;
|
|
@@ -512,6 +518,16 @@ export class SessionManager {
|
|
|
512
518
|
const allThrottled = bestScore >= 1;
|
|
513
519
|
const RECENCY_GAP_MS = allThrottled ? 0 : 1_000;
|
|
514
520
|
const clearWinner = runnerUp === undefined || best.ms + RECENCY_GAP_MS < runnerUp.ms;
|
|
521
|
+
// Healthy tabs heartbeat on the same cadence, so the recency gap between two of them is noise.
|
|
522
|
+
// When exactly one has the window focus it is the one a human is looking at: tabs piled up one
|
|
523
|
+
// per `init` run, and every tool called without a sessionId was refused while one of them was
|
|
524
|
+
// plainly the tab in front. Only among healthy ones — focus says nothing about a wedged tab.
|
|
525
|
+
const focused = allThrottled ? [] : candidates.filter((x) => x.s.health().focused);
|
|
526
|
+
const [onlyFocused, secondFocused] = focused;
|
|
527
|
+
if (!clearWinner && onlyFocused !== undefined && secondFocused === undefined) {
|
|
528
|
+
onlyFocused.s.markAgentActivity();
|
|
529
|
+
return onlyFocused.s;
|
|
530
|
+
}
|
|
515
531
|
if (!clearWinner) {
|
|
516
532
|
// Ambiguous: list sessions with their health so the agent can choose.
|
|
517
533
|
const detail = all
|
|
@@ -22,6 +22,34 @@ export declare function composeEndedNotice(base: string, undelivered: string[]):
|
|
|
22
22
|
* not a crash, since the agent is still connected and revives on its next action. Returns the ids.
|
|
23
23
|
*/
|
|
24
24
|
export declare function reapIdleSessions(sessions: SessionManager): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Drop sessions that have ENDED, gone quiet and lost their socket, so a listing stops offering them.
|
|
27
|
+
*
|
|
28
|
+
* `reticle_session { action: "end" }` sets state and nothing else -- it never calls
|
|
29
|
+
* `sessions.remove`. `SessionManager.list()` does not filter by state, and `reapIdleSessions`
|
|
30
|
+
* above skips ended sessions by design. The only code path in the whole server that removes a
|
|
31
|
+
* session is the socket `close` handler in bridge.ts. So an ended session whose socket is already
|
|
32
|
+
* gone has nothing left that can remove it, and the row is listed for the life of the daemon
|
|
33
|
+
* (#938).
|
|
34
|
+
*
|
|
35
|
+
* That is what the field report describes: `{ ended: true }` three times, the row back on the next
|
|
36
|
+
* `reticle_sessions` call each time, still there after every tab on the origin was closed, 17 hours
|
|
37
|
+
* attached. And Reticle's own advice for a stale session is
|
|
38
|
+
* `Call reticle_session{action:"end"} to free this session` -- which is the one thing that does not
|
|
39
|
+
* free it.
|
|
40
|
+
*
|
|
41
|
+
* Deliberately NOT "remove on end". The panel is client-side, so `setState` has already pushed the
|
|
42
|
+
* ended state to the browser and the HUD keeps it; but `end` is documented idempotent, and removing
|
|
43
|
+
* immediately would make a second call fail to resolve. Waiting for the session to go quiet keeps
|
|
44
|
+
* that, keeps a just-ended session inspectable, and still collects the case that actually hurts: a
|
|
45
|
+
* row nobody can get rid of.
|
|
46
|
+
*
|
|
47
|
+
* Only once the socket is no longer open. An ended session whose tab is still connected but quiet
|
|
48
|
+
* belongs to a page that holds a live connection; dropping the record would orphan that socket,
|
|
49
|
+
* leaving a connection the daemon no longer tracks. Such a session is still removed the ordinary
|
|
50
|
+
* way, by the `close` handler, when the tab goes.
|
|
51
|
+
*/
|
|
52
|
+
export declare function reapEndedSessions(sessions: SessionManager, staleAfterMs?: number): string[];
|
|
25
53
|
/** End every active session immediately (the agent / MCP client disconnected → WARN). Returns ended ids. */
|
|
26
54
|
export declare function endAllSessions(sessions: SessionManager, reason: string): string[];
|
|
27
55
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PresenterTone, SESSION_LIFECYCLE, UNDELIVERED_NOTES_LABEL } from '@reticlehq/core';
|
|
1
|
+
import { PresenterTone, SESSION_LEASE, SESSION_LIFECYCLE, UNDELIVERED_NOTES_LABEL, } from '@reticlehq/core';
|
|
2
2
|
import { log } from '../../log.js';
|
|
3
3
|
/**
|
|
4
4
|
* Server-authoritative session liveness. The browser idle timer is throttled in a backgrounded tab
|
|
@@ -51,6 +51,50 @@ export function reapIdleSessions(sessions) {
|
|
|
51
51
|
log('session_reaped_idle', { sessions: ended });
|
|
52
52
|
return ended;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Drop sessions that have ENDED, gone quiet and lost their socket, so a listing stops offering them.
|
|
56
|
+
*
|
|
57
|
+
* `reticle_session { action: "end" }` sets state and nothing else -- it never calls
|
|
58
|
+
* `sessions.remove`. `SessionManager.list()` does not filter by state, and `reapIdleSessions`
|
|
59
|
+
* above skips ended sessions by design. The only code path in the whole server that removes a
|
|
60
|
+
* session is the socket `close` handler in bridge.ts. So an ended session whose socket is already
|
|
61
|
+
* gone has nothing left that can remove it, and the row is listed for the life of the daemon
|
|
62
|
+
* (#938).
|
|
63
|
+
*
|
|
64
|
+
* That is what the field report describes: `{ ended: true }` three times, the row back on the next
|
|
65
|
+
* `reticle_sessions` call each time, still there after every tab on the origin was closed, 17 hours
|
|
66
|
+
* attached. And Reticle's own advice for a stale session is
|
|
67
|
+
* `Call reticle_session{action:"end"} to free this session` -- which is the one thing that does not
|
|
68
|
+
* free it.
|
|
69
|
+
*
|
|
70
|
+
* Deliberately NOT "remove on end". The panel is client-side, so `setState` has already pushed the
|
|
71
|
+
* ended state to the browser and the HUD keeps it; but `end` is documented idempotent, and removing
|
|
72
|
+
* immediately would make a second call fail to resolve. Waiting for the session to go quiet keeps
|
|
73
|
+
* that, keeps a just-ended session inspectable, and still collects the case that actually hurts: a
|
|
74
|
+
* row nobody can get rid of.
|
|
75
|
+
*
|
|
76
|
+
* Only once the socket is no longer open. An ended session whose tab is still connected but quiet
|
|
77
|
+
* belongs to a page that holds a live connection; dropping the record would orphan that socket,
|
|
78
|
+
* leaving a connection the daemon no longer tracks. Such a session is still removed the ordinary
|
|
79
|
+
* way, by the `close` handler, when the tab goes.
|
|
80
|
+
*/
|
|
81
|
+
export function reapEndedSessions(sessions, staleAfterMs = SESSION_LEASE.STALE_AFTER_MS) {
|
|
82
|
+
const dropped = [];
|
|
83
|
+
// Snapshot first: `remove` mutates the map this iterates.
|
|
84
|
+
for (const session of [...sessions.all()]) {
|
|
85
|
+
if (!session.isEnded())
|
|
86
|
+
continue;
|
|
87
|
+
if (session.socketOpen())
|
|
88
|
+
continue;
|
|
89
|
+
if (session.staleMs() < staleAfterMs)
|
|
90
|
+
continue;
|
|
91
|
+
if (sessions.remove(session))
|
|
92
|
+
dropped.push(session.id);
|
|
93
|
+
}
|
|
94
|
+
if (dropped.length > 0)
|
|
95
|
+
log('session_reaped_ended', { sessions: dropped });
|
|
96
|
+
return dropped;
|
|
97
|
+
}
|
|
54
98
|
/** End every active session immediately (the agent / MCP client disconnected → WARN). Returns ended ids. */
|
|
55
99
|
export function endAllSessions(sessions, reason) {
|
|
56
100
|
const ended = [];
|
|
@@ -81,6 +125,10 @@ export class SessionReaper {
|
|
|
81
125
|
return;
|
|
82
126
|
this.#timer = setInterval(() => {
|
|
83
127
|
reapIdleSessions(this.#sessions);
|
|
128
|
+
// Second rule, same sweep, kept as its own function: one ends a live session that went
|
|
129
|
+
// quiet, the other collects one that already ended. Merging them would put "end this" and
|
|
130
|
+
// "forget this" behind a single condition.
|
|
131
|
+
reapEndedSessions(this.#sessions);
|
|
84
132
|
}, this.#intervalMs);
|
|
85
133
|
this.#timer.unref();
|
|
86
134
|
}
|
|
@@ -91,6 +91,8 @@ export declare class Session implements HandshakeFacts {
|
|
|
91
91
|
* `reticle_session{action:"end"}` — a flag on the record, not on the page — could not help.
|
|
92
92
|
*/
|
|
93
93
|
unresponsive(): boolean;
|
|
94
|
+
/** False once the socket is closing or closed: dropping this record then orphans nothing. */
|
|
95
|
+
socketOpen(): boolean;
|
|
94
96
|
/** Wall-clock age of the session in milliseconds. */
|
|
95
97
|
staleMs(): number;
|
|
96
98
|
/** The document the most recent stamped event was observed under. See `#documentId`. */
|
|
@@ -114,6 +116,9 @@ export declare class Session implements HandshakeFacts {
|
|
|
114
116
|
* against the same control after a re-render gave it a different ref.
|
|
115
117
|
*/
|
|
116
118
|
private recordActedLabelFrom;
|
|
119
|
+
/** A control whose action PROVED its declared consequence — the coverage ledger's `proved`. */
|
|
120
|
+
recordProvedFrom(actPayload: unknown): void;
|
|
121
|
+
provedLabels(): ReadonlySet<string>;
|
|
117
122
|
actedRefs(): ReadonlySet<string>;
|
|
118
123
|
noteRateLimited(dropped: number): void;
|
|
119
124
|
blindSpots(): Readonly<Record<string, number>>;
|
|
@@ -211,16 +216,13 @@ export declare class Session implements HandshakeFacts {
|
|
|
211
216
|
total: number;
|
|
212
217
|
dropped: number;
|
|
213
218
|
};
|
|
219
|
+
/** Did the event store lose scarce evidence from a window opened at `cursor`? See `windowLost`. */
|
|
220
|
+
lostSince(cursor: number): boolean;
|
|
214
221
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* moves for the age and churn evictions that every live page produces continuously. The journal is
|
|
218
|
-
* the other half of that store, so a durable read that could not reach back to `cursor` lost
|
|
219
|
-
* exactly what an eviction would have. Both boundaries are INCLUSIVE (`t` is a millisecond many
|
|
220
|
-
* records share) and an absent `lostThroughT` impeaches every window rather than none — the
|
|
221
|
-
* conservative direction, both times.
|
|
222
|
+
* Hold events at/after `cursor` against count-cap eviction until the returned function runs, so a
|
|
223
|
+
* predicate's own match cannot be dropped inside the window it is graded on (#668).
|
|
222
224
|
*/
|
|
223
|
-
|
|
225
|
+
protectWindow(cursor: number): () => void;
|
|
224
226
|
onEvent(listener: (event: ReticleEvent) => void): () => void;
|
|
225
227
|
onDisconnect(listener: () => void): () => void;
|
|
226
228
|
/** Send a command to the browser and await its reply (or time out). */
|
|
@@ -14,9 +14,9 @@ import { span } from '../../trace.js';
|
|
|
14
14
|
import { keepCallerContextInDaemon, noteToDaemonLog } from './daemon-answers.js';
|
|
15
15
|
import { isKnownRealm, EventType, HumanControlDataSchema, HumanControlKind, HumanMarkDataSchema, HudUseDataSchema, ReticleCommand, MessageKind, parseEventPayload, PresenterTone, SESSION_HEALTH, SESSION_LEASE, SESSION_LIFECYCLE, SessionState, } from '@reticlehq/core';
|
|
16
16
|
import { RingBuffer } from '@reticlehq/engine/window/ring-buffer.js';
|
|
17
|
-
import { readJournalWriteLoss, readQueryEvents, } from '../../memory/journal/journal-query.js';
|
|
17
|
+
import { readJournalWriteLoss, readQueryEvents, windowLost, } from '../../memory/journal/journal-query.js';
|
|
18
18
|
import {} from '@reticlehq/engine/window/ambient.js';
|
|
19
|
-
import { ObservedState } from './facts/observed-state.js';
|
|
19
|
+
import { ObservedState, controlLabelsOf } from './facts/observed-state.js';
|
|
20
20
|
import { recordBrowserLatency, recordHudUse, recordSdkFailure, } from '../../telemetry/session-metrics.js';
|
|
21
21
|
import { LiveControl } from './human/live-control.js';
|
|
22
22
|
import { ReviewStore } from './human/review-store.js';
|
|
@@ -241,6 +241,10 @@ export class Session {
|
|
|
241
241
|
unresponsive() {
|
|
242
242
|
return this.#unansweredCommands >= UNRESPONSIVE_AFTER_TIMEOUTS;
|
|
243
243
|
}
|
|
244
|
+
/** False once the socket is closing or closed: dropping this record then orphans nothing. */
|
|
245
|
+
socketOpen() {
|
|
246
|
+
return this.#socket.readyState === WS_OPEN;
|
|
247
|
+
}
|
|
244
248
|
/** Wall-clock age of the session in milliseconds. */
|
|
245
249
|
staleMs() {
|
|
246
250
|
return this.#clock() - this.#startedAt;
|
|
@@ -352,24 +356,16 @@ export class Session {
|
|
|
352
356
|
* against the same control after a re-render gave it a different ref.
|
|
353
357
|
*/
|
|
354
358
|
recordActedLabelFrom(result) {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
this.#observed.recordActedLabel(testid);
|
|
366
|
-
const role = record['role'];
|
|
367
|
-
const name = record['name'];
|
|
368
|
-
if (typeof role !== 'string' || typeof name !== 'string')
|
|
369
|
-
return;
|
|
370
|
-
if (0 === role.length || 0 === name.length)
|
|
371
|
-
return;
|
|
372
|
-
this.#observed.recordActedLabel(`${role} "${name}"`);
|
|
359
|
+
for (const label of controlLabelsOf(result.result))
|
|
360
|
+
this.#observed.recordActedLabel(label);
|
|
361
|
+
}
|
|
362
|
+
/** A control whose action PROVED its declared consequence — the coverage ledger's `proved`. */
|
|
363
|
+
recordProvedFrom(actPayload) {
|
|
364
|
+
for (const label of controlLabelsOf(actPayload))
|
|
365
|
+
this.#observed.recordProvedLabel(label);
|
|
366
|
+
}
|
|
367
|
+
provedLabels() {
|
|
368
|
+
return this.#observed.provedLabels();
|
|
373
369
|
}
|
|
374
370
|
actedRefs() {
|
|
375
371
|
return this.#observed.actedRefs();
|
|
@@ -532,20 +528,16 @@ export class Session {
|
|
|
532
528
|
bufferHealth() {
|
|
533
529
|
return this.#buffer.bufferHealth();
|
|
534
530
|
}
|
|
531
|
+
/** Did the event store lose scarce evidence from a window opened at `cursor`? See `windowLost`. */
|
|
532
|
+
lostSince(cursor) {
|
|
533
|
+
return windowLost(this.#journalReader, this.#buffer, cursor);
|
|
534
|
+
}
|
|
535
535
|
/**
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
* moves for the age and churn evictions that every live page produces continuously. The journal is
|
|
539
|
-
* the other half of that store, so a durable read that could not reach back to `cursor` lost
|
|
540
|
-
* exactly what an eviction would have. Both boundaries are INCLUSIVE (`t` is a millisecond many
|
|
541
|
-
* records share) and an absent `lostThroughT` impeaches every window rather than none — the
|
|
542
|
-
* conservative direction, both times.
|
|
536
|
+
* Hold events at/after `cursor` against count-cap eviction until the returned function runs, so a
|
|
537
|
+
* predicate's own match cannot be dropped inside the window it is graded on (#668).
|
|
543
538
|
*/
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
return true;
|
|
547
|
-
const lost = this.#journalReader?.readLoss?.();
|
|
548
|
-
return lost !== undefined && (lost.lostThroughT === undefined || lost.lostThroughT >= cursor);
|
|
539
|
+
protectWindow(cursor) {
|
|
540
|
+
return this.#buffer.protect(cursor);
|
|
549
541
|
}
|
|
550
542
|
onEvent(listener) {
|
|
551
543
|
this.#listeners.add(listener);
|
|
@@ -8,9 +8,10 @@ export interface SharedServer {
|
|
|
8
8
|
* so each concurrent Claude Code client needs its own McpServer.
|
|
9
9
|
* Must be called before listen.
|
|
10
10
|
*/
|
|
11
|
-
attachMcp(factory: () => McpServer): void;
|
|
11
|
+
attachMcp(factory: (peerSkew?: string) => McpServer): void;
|
|
12
12
|
/** Register the JSON the daemon returns from GET /status (live sessions + health for `reticle status`). */
|
|
13
|
-
|
|
13
|
+
/** `held`: the request carried STATUS_HOLD_QUERY — a setup run is waiting on this daemon. */
|
|
14
|
+
attachStatus(provider: (held: boolean) => unknown): void;
|
|
14
15
|
/**
|
|
15
16
|
* Register the handler behind POST /drive: open a driveable browser at `url` and answer with the
|
|
16
17
|
* session it registered.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as http from 'node:http';
|
|
2
2
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
3
|
-
import { MCP_SSE_PATH, MCP_MESSAGE_PATH, MCP_SHUTDOWN_EVENT, STATUS_PATH, DRIVE_PATH, } from '@reticlehq/core';
|
|
3
|
+
import { MCP_SSE_PATH, MCP_MESSAGE_PATH, MCP_SHUTDOWN_EVENT, STATUS_PATH, STATUS_HOLD_QUERY, DRIVE_PATH, } from '@reticlehq/core';
|
|
4
4
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
5
|
-
import {
|
|
5
|
+
import { agentPeerSkew, PEER_VERSION_PARAM, PEER_CONTRACT_PARAM, } from '../command/version/peer-announce.js';
|
|
6
6
|
import { log } from '../log.js';
|
|
7
7
|
import { isLoopbackHost, isLocalWebOrigin, isLoopbackPeer, requestToken, tokensMatch, } from '../portal/bridge/token-auth.js';
|
|
8
8
|
import { LOOPBACK_IDLE_MS } from './loopback-agent.js';
|
|
@@ -76,7 +76,9 @@ export function createSharedServer(options = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
if ('GET' === req.method && path === STATUS_PATH) {
|
|
79
|
-
|
|
79
|
+
// A setup run waiting on this daemon says so on its polls; see daemon-usefulness.
|
|
80
|
+
const held = url.searchParams.has(STATUS_HOLD_QUERY);
|
|
81
|
+
const body = JSON.stringify(statusProvider?.(held) ?? { running: true });
|
|
80
82
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
81
83
|
res.end(body);
|
|
82
84
|
return;
|
|
@@ -139,7 +141,7 @@ export function createSharedServer(options = {}) {
|
|
|
139
141
|
// own check (a stderr line no agent reads), a nudge queued here rides out on the agent's next
|
|
140
142
|
// tool result. That is the pair the user hits after `npm update`: a cached npx MCP package
|
|
141
143
|
// talking to a daemon from a different build.
|
|
142
|
-
|
|
144
|
+
const peerSkew = agentPeerSkew(url.searchParams.get(PEER_VERSION_PARAM), url.searchParams.get(PEER_CONTRACT_PARAM));
|
|
143
145
|
if (mcpFactory === undefined) {
|
|
144
146
|
res.writeHead(503, { 'Content-Type': 'text/plain' });
|
|
145
147
|
res.end('MCP server not ready');
|
|
@@ -148,7 +150,8 @@ export function createSharedServer(options = {}) {
|
|
|
148
150
|
// Fresh McpServer per connection: the MCP SDK's Protocol layer only supports
|
|
149
151
|
// one active transport per Server instance, so concurrent clients each need
|
|
150
152
|
// their own instance backed by the same shared ToolDeps.
|
|
151
|
-
|
|
153
|
+
// Handed to THIS connection's server only: the skew is about this agent, not the daemon (#1136).
|
|
154
|
+
const mcpServer = mcpFactory(peerSkew);
|
|
152
155
|
const transport = new SSEServerTransport(MCP_MESSAGE_PATH, res);
|
|
153
156
|
const sid = transport.sessionId;
|
|
154
157
|
transports.set(sid, transport);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export { SseFrameParser, type SseFrame } from './sse-frame-parser.js';
|
|
2
|
-
export { probeDaemon, waitForDaemon } from './proxy/proxy-daemon-probe.js';
|
|
2
|
+
export { probeDaemon, waitForDaemon, waitForDaemonBind } from './proxy/proxy-daemon-probe.js';
|
|
3
3
|
/**
|
|
4
4
|
* Re-exported so every existing caller (and every spec) keeps importing the proxy's log from the
|
|
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/proxy-log.js';
|
|
8
|
-
export { reconnectDelayMs
|
|
8
|
+
export { reconnectDelayMs } from './proxy/proxy-backoff.js';
|
|
9
9
|
export { MCP_PROXY_HTTP_AGENT_OPTIONS, postToSession, shouldRetryUnsentPost, type PostFailure, } from './proxy/mcp-post-transport.js';
|
|
10
10
|
/**
|
|
11
11
|
* Overridable so the survival spec can reach exhaustion in seconds instead of minutes — the same
|
|
@@ -6,7 +6,7 @@ import { toolsChangedNotification } from './proxy/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';
|
|
8
8
|
export { SseFrameParser } from './sse-frame-parser.js';
|
|
9
|
-
export { probeDaemon, waitForDaemon } from './proxy/proxy-daemon-probe.js';
|
|
9
|
+
export { probeDaemon, waitForDaemon, waitForDaemonBind } from './proxy/proxy-daemon-probe.js';
|
|
10
10
|
import { probeDaemon } from './proxy/proxy-daemon-probe.js';
|
|
11
11
|
import { SERVER_VERSION } from '../../command/version/identity/server-version.js';
|
|
12
12
|
import { buildServerInstructions } from './server-instructions.js';
|
|
@@ -33,7 +33,7 @@ import { OutageReason, OutageStage, reportMcpOutage } from './faults/mcp-outage.
|
|
|
33
33
|
import { postToSession } from './proxy/mcp-post-transport.js';
|
|
34
34
|
import { reconnectDelayMs } from './proxy/proxy-backoff.js';
|
|
35
35
|
import { PROXY_IDLE_EXIT_EVENT, ProxyIdleExit, resolveProxyIdleExitMs } from './proxy-idle-exit.js';
|
|
36
|
-
export { reconnectDelayMs
|
|
36
|
+
export { reconnectDelayMs } from './proxy/proxy-backoff.js';
|
|
37
37
|
export { MCP_PROXY_HTTP_AGENT_OPTIONS, postToSession, shouldRetryUnsentPost, } from './proxy/mcp-post-transport.js';
|
|
38
38
|
/**
|
|
39
39
|
* How many consecutive failed reconnects before the proxy stops RETRYING. It does not stop serving:
|
package/dist/surface/mcp/mcp.js
CHANGED
|
@@ -103,7 +103,7 @@ import { firstSentence } from '../tools/first-sentence.js';
|
|
|
103
103
|
* making it a pointer costs nothing.
|
|
104
104
|
*/
|
|
105
105
|
const PREDICATE_KINDS = 'Predicate object: { kind, ...fields }. kind is one of element | text | net | route | console | ' +
|
|
106
|
-
'animation | signal | state | settled | allOf | anyOf | not.';
|
|
106
|
+
'animation | signal | state | settled | compare | allOf | anyOf | not.';
|
|
107
107
|
/**
|
|
108
108
|
* Said once per turn: the bug-catching options, and where to get the rest of the field grammar.
|
|
109
109
|
*
|
|
@@ -118,9 +118,11 @@ const PREDICATE_KINDS = 'Predicate object: { kind, ...fields }. kind is one of e
|
|
|
118
118
|
* paying for six.
|
|
119
119
|
*/
|
|
120
120
|
const PREDICATE_FIELD_GRAMMAR_HINT = ' Bug-catching options: net.count (exact request count — catches double-submit), ' +
|
|
121
|
-
'net.bodyMatches (a
|
|
121
|
+
'net.bodyMatches (a JSON field match on what the SERVER answered — say the FIELD (a dotted path reaches nested ones) and its value, ' +
|
|
122
122
|
'because a substring of the body matches key names too), net.requestBodyMatches (the same on what ' +
|
|
123
123
|
'the UI SENT — the verdict for "applying this filter actually puts it in the payload"), ' +
|
|
124
|
+
'compare (page text vs server answer, no expected value: { left:{from:"text",scope}, ' +
|
|
125
|
+
'right:{from:"net",urlContains,path}, as:"number" }), ' +
|
|
124
126
|
'console.absent:true (action completed with a CLEAN ' +
|
|
125
127
|
'console), absent:true on element/text (it should be gone). Call reticle_tools for the full field ' +
|
|
126
128
|
'grammar of a kind.';
|
|
@@ -660,7 +662,7 @@ tools = tableForSurface(profile)) {
|
|
|
660
662
|
content: [
|
|
661
663
|
{
|
|
662
664
|
type: 'text',
|
|
663
|
-
text: JSON.stringify(liveCallValues(takeVersionSkewOnto(buildErrorPayload(message)), advertisedNames)),
|
|
665
|
+
text: JSON.stringify(liveCallValues(takeVersionSkewOnto(buildErrorPayload(message), deps.peerSkew), advertisedNames)),
|
|
664
666
|
},
|
|
665
667
|
],
|
|
666
668
|
};
|