@reticlehq/server 2.6.0 → 2.8.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/README.md +14 -5
- package/SKILL.md +251 -33
- package/dist/bridge/bridge.d.ts +6 -1
- package/dist/bridge/bridge.js +27 -4
- package/dist/capsule/causal-summary.d.ts +20 -0
- package/dist/capsule/causal-summary.js +31 -6
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +68 -0
- package/dist/cli/chromium-hint.js +145 -0
- package/dist/cli/cli-doctor.js +31 -36
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-launch.d.ts +18 -0
- package/dist/cli/cli-launch.js +32 -1
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +8 -0
- package/dist/cli/cli-port.js +36 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/doctor-daemon-line.js +10 -1
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +66 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli.js +139 -23
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/daemon/daemon-resilience.js +2 -2
- package/dist/daemon/loopback-alias.d.ts +32 -0
- package/dist/daemon/loopback-alias.js +80 -0
- package/dist/events/contradictions.d.ts +14 -0
- package/dist/events/contradictions.js +26 -17
- package/dist/events/predicate-eval.d.ts +26 -3
- package/dist/events/predicate-eval.js +284 -95
- package/dist/events/predicate-parse.d.ts +1 -1
- package/dist/events/predicate-parse.js +18 -4
- package/dist/events/predicate.js +170 -18
- package/dist/events/ring-buffer.d.ts +8 -0
- package/dist/events/ring-buffer.js +34 -1
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-tools.js +12 -4
- package/dist/flows/heal-run.js +1 -1
- package/dist/honesty/blind-spots.js +6 -2
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +9 -1
- package/dist/honesty/unsettled.d.ts +40 -0
- package/dist/honesty/unsettled.js +78 -0
- package/dist/honesty/verified.d.ts +7 -0
- package/dist/honesty/verified.js +27 -4
- package/dist/http-server.js +11 -1
- package/dist/index.js +29 -2
- package/dist/init/agent-rules.d.ts +16 -0
- package/dist/init/agent-rules.js +96 -17
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +11 -5
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +96 -0
- package/dist/init/confirm.js +138 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +15 -0
- package/dist/init/non-js-project.d.ts +27 -0
- package/dist/init/non-js-project.js +84 -0
- package/dist/init/plan-framework.d.ts +9 -0
- package/dist/init/plan-framework.js +72 -16
- package/dist/init/plan.d.ts +40 -4
- package/dist/init/plan.js +183 -43
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +181 -46
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +46 -6
- package/dist/init/snippets.d.ts +64 -5
- package/dist/init/snippets.js +220 -31
- package/dist/init/vite-config.js +29 -3
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +29 -4
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +69 -3
- package/dist/mcp/mcp-proxy.js +215 -23
- package/dist/mcp/mcp.js +58 -7
- package/dist/mcp/proxy-lifecycle.d.ts +20 -1
- package/dist/mcp/proxy-lifecycle.js +22 -1
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/pool/playwright-launcher.js +10 -2
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/session/live-control-tools.js +39 -2
- package/dist/session/live-control.d.ts +7 -9
- package/dist/session/live-control.js +34 -1
- package/dist/session/no-session-diagnosis.d.ts +23 -4
- package/dist/session/no-session-diagnosis.js +128 -44
- package/dist/session/no-session-watch.js +12 -0
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/review-store.d.ts +19 -0
- package/dist/session/review-store.js +34 -5
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +9 -0
- package/dist/session/session-manager.js +11 -0
- package/dist/session/session.d.ts +8 -0
- package/dist/session/session.js +12 -0
- package/dist/status-payload.d.ts +13 -1
- package/dist/status-payload.js +6 -1
- package/dist/telemetry/app-instrumented.d.ts +37 -0
- package/dist/telemetry/app-instrumented.js +54 -0
- package/dist/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +0 -20
- package/dist/telemetry/bug-found.js +38 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/daemon-telemetry.d.ts +4 -4
- package/dist/telemetry/daemon-telemetry.js +8 -4
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +36 -0
- package/dist/telemetry/install-source.js +40 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.d.ts +3 -3
- package/dist/telemetry/session-metrics.js +9 -4
- package/dist/telemetry/telemetry.d.ts +7 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +72 -8
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +77 -11
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +25 -1
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +1 -1
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +70 -5
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +149 -32
- package/dist/tools/lease-tools.d.ts +29 -1
- package/dist/tools/lease-tools.js +58 -5
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +1 -1
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +48 -0
- package/dist/tools/react-grace.js +87 -0
- package/dist/tools/read-tools.js +20 -4
- package/dist/tools/settle-in-flight.d.ts +79 -0
- package/dist/tools/settle-in-flight.js +106 -0
- package/dist/tools/snapshot-delta.d.ts +7 -0
- package/dist/tools/snapshot-delta.js +130 -6
- package/dist/tools/tool-kit.d.ts +52 -5
- package/dist/tools/tool-kit.js +55 -24
- package/dist/tools/tool-surface.js +28 -9
- package/dist/tools/tools.js +38 -4
- package/dist/visual/capture-cleanup.d.ts +16 -0
- package/dist/visual/capture-cleanup.js +39 -0
- package/dist/visual/visual-tools.d.ts +18 -0
- package/dist/visual/visual-tools.js +24 -5
- package/docs/README.md +56 -11
- package/docs/actions.mdx +87 -0
- package/docs/agent-cheatsheet.md +102 -26
- package/docs/architecture.md +46 -35
- package/docs/benchmarks.md +76 -72
- package/docs/best-practices.mdx +211 -0
- package/docs/capabilities.mdx +387 -0
- package/docs/cli/affected.mdx +51 -0
- package/docs/cli/capsules.mdx +69 -0
- package/docs/cli/cloud.mdx +127 -0
- package/docs/cli/doctor.mdx +76 -0
- package/docs/cli/drive.mdx +55 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +92 -0
- package/docs/cli/hunt.mdx +57 -0
- package/docs/cli/identify.mdx +62 -0
- package/docs/cli/init.mdx +94 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +68 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +62 -0
- package/docs/cli/status.mdx +64 -0
- package/docs/cli/stop.mdx +56 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +57 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +55 -0
- package/docs/cli.mdx +219 -0
- package/docs/debugging.md +13 -9
- package/docs/deploy-checks.md +15 -11
- package/docs/desktop-apps.md +49 -40
- package/docs/desktop.mdx +188 -0
- package/docs/docs.json +363 -0
- package/docs/enterprise.md +20 -16
- package/docs/faq.mdx +88 -0
- package/docs/fixtures.md +11 -7
- package/docs/flows.md +26 -22
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +199 -0
- package/docs/gate-plan.md +45 -41
- package/docs/gates.md +23 -19
- package/docs/getting-started.md +76 -65
- package/docs/human-control.md +22 -18
- package/docs/index.mdx +101 -0
- package/docs/install-agentic.mdx +215 -0
- package/docs/install-manual.mdx +194 -0
- package/docs/instrumentation.mdx +232 -0
- package/docs/integration-patterns.md +38 -34
- package/docs/local-registry.md +16 -12
- package/docs/multi-agent-testing.md +16 -12
- package/docs/packages/babel-plugin.mdx +70 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +106 -0
- package/docs/packages/electron.mdx +87 -0
- package/docs/packages/eslint-plugin.mdx +84 -0
- package/docs/packages/next.mdx +91 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +108 -0
- package/docs/packages/vite-plugin.mdx +96 -0
- package/docs/packages.mdx +217 -0
- package/docs/platform-integration.md +33 -26
- package/docs/predicates.mdx +179 -0
- package/docs/quickstart.mdx +211 -0
- package/docs/recipes.mdx +242 -0
- package/docs/skill-file.mdx +105 -0
- package/docs/state-management.mdx +197 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +47 -32
- package/docs/telemetry-contract.md +101 -46
- package/docs/telemetry-events.mdx +430 -0
- package/docs/telemetry.md +63 -50
- package/docs/testing.md +10 -6
- package/docs/token-efficiency.md +30 -26
- package/docs/tools-act-and-wait.mdx +123 -0
- package/docs/tools-act-sequence.mdx +120 -0
- package/docs/tools-act.mdx +82 -0
- package/docs/tools-assert.mdx +70 -0
- package/docs/tools-console.mdx +59 -0
- package/docs/tools-inspect.mdx +85 -0
- package/docs/tools-navigate.mdx +63 -0
- package/docs/tools-network.mdx +86 -0
- package/docs/tools-observe.mdx +89 -0
- package/docs/tools-overview.mdx +157 -0
- package/docs/tools-query.mdx +82 -0
- package/docs/tools-session-and-feedback.mdx +91 -0
- package/docs/tools-sessions.mdx +65 -0
- package/docs/tools-snapshot.mdx +75 -0
- package/docs/tools-state.mdx +81 -0
- package/docs/tools-tools-and-run.mdx +125 -0
- package/docs/tools-wait-for.mdx +66 -0
- package/docs/troubleshooting.mdx +193 -0
- package/docs/usage.md +175 -153
- package/docs/vs-chrome-devtools-mcp.mdx +82 -0
- package/docs/vs-playwright-mcp.mdx +85 -0
- package/docs/vs-screenshots.mdx +96 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +107 -0
- package/package.json +2 -2
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- package/docs/mint.json +0 -61
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { BrowserLaunchKind } from '@reticlehq/core';
|
|
9
9
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
10
10
|
import { classifyConnectFailure } from '../telemetry/connect-failure.js';
|
|
11
|
+
import { chromiumInstallCommand, bundledPlaywrightVersion } from '../cli/chromium-hint.js';
|
|
11
12
|
/**
|
|
12
13
|
* How a leased tab navigates. Pure, and exported, because the decision in it is worth a test while
|
|
13
14
|
* the rest of this file needs a real Chromium.
|
|
@@ -48,8 +49,15 @@ function wrapBrowser(browser) {
|
|
|
48
49
|
onDisconnected: (handler) => browser.on('disconnected', handler),
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
|
-
/**
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The fix for the most common first-run failure: Chromium isn't installed for Playwright.
|
|
54
|
+
*
|
|
55
|
+
* Pinned to the playwright this process bundles. Unpinned, `npx` fetches the latest playwright and
|
|
56
|
+
* installs ITS browser revision — a build this launcher will never look at — so the user follows the
|
|
57
|
+
* advice, it succeeds, and the next launch fails identically. Shares one builder with `doctor` so the
|
|
58
|
+
* two cannot suggest different commands for the same missing browser.
|
|
59
|
+
*/
|
|
60
|
+
const CHROMIUM_MISSING_HINT = `Chromium is not installed for Playwright. Run: ${chromiumInstallCommand(bundledPlaywrightVersion())}`;
|
|
53
61
|
/** A Launcher that boots a real headless Chromium and adapts it to the pool's interface. */
|
|
54
62
|
export function playwrightLauncher(opts = {}) {
|
|
55
63
|
const headless = opts.headless ?? true;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ReticleEvent } from '@reticlehq/core';
|
|
2
|
+
interface RouteLearningSession {
|
|
3
|
+
url: string;
|
|
4
|
+
onEvent(handler: (event: ReticleEvent) => void): () => void;
|
|
5
|
+
}
|
|
6
|
+
interface RouteLearningBridge {
|
|
7
|
+
sessions: {
|
|
8
|
+
all(): RouteLearningSession[];
|
|
9
|
+
};
|
|
10
|
+
attachSessionReady(handler: (session: RouteLearningSession) => void): void;
|
|
11
|
+
}
|
|
12
|
+
interface RouteLearningStore {
|
|
13
|
+
recordRoutes(routes: readonly string[]): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/** Coalesce navigation bursts so one crawl does not lock and rewrite project.json per event. */
|
|
16
|
+
export declare const ROUTE_PERSIST_DEBOUNCE_MS = 50;
|
|
17
|
+
/** Convert a validated route-change payload into a bounded route identity. */
|
|
18
|
+
export declare function routeFromEvent(event: ReticleEvent): string | undefined;
|
|
19
|
+
/** Read the same route shape from a session's absolute URL. */
|
|
20
|
+
export declare function routeFromUrl(value: string): string | undefined;
|
|
21
|
+
export declare function routesFromEvents(events: readonly ReticleEvent[]): string[];
|
|
22
|
+
/** Persist initial and subsequent routes for existing and future browser sessions. */
|
|
23
|
+
export declare function attachRouteLearning(bridge: RouteLearningBridge, project: RouteLearningStore): void;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { EventType } from '@reticlehq/core';
|
|
2
|
+
/** Coalesce navigation bursts so one crawl does not lock and rewrite project.json per event. */
|
|
3
|
+
export const ROUTE_PERSIST_DEBOUNCE_MS = 50;
|
|
4
|
+
/** Convert a validated route-change payload into a bounded route identity. */
|
|
5
|
+
export function routeFromEvent(event) {
|
|
6
|
+
if (event.type !== EventType.ROUTE_CHANGE)
|
|
7
|
+
return undefined;
|
|
8
|
+
const pathname = event.data['pathname'];
|
|
9
|
+
if ('string' !== typeof pathname || 0 === pathname.length)
|
|
10
|
+
return undefined;
|
|
11
|
+
return pathname;
|
|
12
|
+
}
|
|
13
|
+
/** Read the same route shape from a session's absolute URL. */
|
|
14
|
+
export function routeFromUrl(value) {
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(value);
|
|
17
|
+
return url.pathname;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function routesFromEvents(events) {
|
|
24
|
+
return events.flatMap((event) => {
|
|
25
|
+
const route = routeFromEvent(event);
|
|
26
|
+
return route === undefined ? [] : [route];
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Persist initial and subsequent routes for existing and future browser sessions. */
|
|
30
|
+
export function attachRouteLearning(bridge, project) {
|
|
31
|
+
const attached = new WeakSet();
|
|
32
|
+
const pending = new Set();
|
|
33
|
+
let flushTimer;
|
|
34
|
+
const flush = () => {
|
|
35
|
+
flushTimer = undefined;
|
|
36
|
+
const routes = [...pending];
|
|
37
|
+
pending.clear();
|
|
38
|
+
if (routes.length > 0)
|
|
39
|
+
void project.recordRoutes(routes).catch(() => undefined);
|
|
40
|
+
};
|
|
41
|
+
const queue = (route) => {
|
|
42
|
+
pending.add(route);
|
|
43
|
+
if (flushTimer !== undefined)
|
|
44
|
+
clearTimeout(flushTimer);
|
|
45
|
+
flushTimer = setTimeout(flush, ROUTE_PERSIST_DEBOUNCE_MS);
|
|
46
|
+
};
|
|
47
|
+
const attach = (session) => {
|
|
48
|
+
if (attached.has(session))
|
|
49
|
+
return;
|
|
50
|
+
attached.add(session);
|
|
51
|
+
const initial = routeFromUrl(session.url);
|
|
52
|
+
if (initial !== undefined)
|
|
53
|
+
queue(initial);
|
|
54
|
+
session.onEvent((event) => {
|
|
55
|
+
const route = routeFromEvent(event);
|
|
56
|
+
if (route !== undefined)
|
|
57
|
+
queue(route);
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
for (const session of bridge.sessions.all())
|
|
61
|
+
attach(session);
|
|
62
|
+
bridge.attachSessionReady(attach);
|
|
63
|
+
}
|
|
@@ -26,6 +26,12 @@ export declare class ProjectStore {
|
|
|
26
26
|
* (last PER_NAME of any one name, then cap to TOTAL overall) and writes byte-stably.
|
|
27
27
|
*/
|
|
28
28
|
recordRun(record: Omit<RunRecord, 'at'>): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Add discovered routes to the learned app map. Empty input is a no-op so "nothing observed"
|
|
31
|
+
* does not materialize as `learned.routes: []`; non-empty updates are serialized per file so
|
|
32
|
+
* concurrent browser sessions accumulate rather than overwriting one another.
|
|
33
|
+
*/
|
|
34
|
+
recordRoutes(routes: readonly string[]): Promise<void>;
|
|
29
35
|
/** The most-recent run for `name` (undefined on missing/malformed/none). Powers diff-vs-last. */
|
|
30
36
|
lastRun(name: string): Promise<RunRecord | undefined>;
|
|
31
37
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PROJECT_FILE_VERSION, PROJECT_RUN_CAP, ProjectFileSchema, ProjectReadError, } from '@reticlehq/core';
|
|
1
|
+
import { PROJECT_FILE_VERSION, PROJECT_ROUTE_CAP, PROJECT_RUN_CAP, ProjectFileSchema, ProjectReadError, } from '@reticlehq/core';
|
|
2
2
|
import { reticleDirPaths } from './reticle-dir.js';
|
|
3
3
|
import { withFileLock } from './file-lock.js';
|
|
4
4
|
const JSON_INDENT = 2;
|
|
@@ -77,6 +77,30 @@ export class ProjectStore {
|
|
|
77
77
|
await this.#fs.writeFile(path, this.#serialize(next));
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Add discovered routes to the learned app map. Empty input is a no-op so "nothing observed"
|
|
82
|
+
* does not materialize as `learned.routes: []`; non-empty updates are serialized per file so
|
|
83
|
+
* concurrent browser sessions accumulate rather than overwriting one another.
|
|
84
|
+
*/
|
|
85
|
+
async recordRoutes(routes) {
|
|
86
|
+
const additions = routes.filter((route) => route.length > 0);
|
|
87
|
+
if (0 === additions.length)
|
|
88
|
+
return;
|
|
89
|
+
const path = reticleDirPaths(this.#root).project;
|
|
90
|
+
await withFileLock(path, async () => {
|
|
91
|
+
const existing = await this.read();
|
|
92
|
+
const base = existing.ok ? existing.file : EMPTY_PROJECT;
|
|
93
|
+
const current = base.learned?.routes ?? [];
|
|
94
|
+
const merged = [...new Set([...current, ...additions])].slice(0, PROJECT_ROUTE_CAP);
|
|
95
|
+
if (merged.length === current.length &&
|
|
96
|
+
merged.every((route, index) => route === current[index]))
|
|
97
|
+
return;
|
|
98
|
+
const learned = { ...base.learned, routes: merged };
|
|
99
|
+
const next = { ...base, learned };
|
|
100
|
+
await this.#fs.mkdir(reticleDirPaths(this.#root).root);
|
|
101
|
+
await this.#fs.writeFile(path, this.#serialize(next));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
80
104
|
/** The most-recent run for `name` (undefined on missing/malformed/none). Powers diff-vs-last. */
|
|
81
105
|
async lastRun(name) {
|
|
82
106
|
const read = await this.read();
|
|
@@ -93,12 +93,37 @@ export const LIVE_CONTROL_TOOLS = [
|
|
|
93
93
|
description: 'Drain and return any messages the human queued from the panel since the last poll. Use to ' +
|
|
94
94
|
'explicitly check for human guidance without acting.',
|
|
95
95
|
inputSchema: { ...sessionIdShape },
|
|
96
|
-
outputSchema: {
|
|
96
|
+
outputSchema: {
|
|
97
|
+
messages: z.array(z.unknown()),
|
|
98
|
+
delivered: z
|
|
99
|
+
.array(z.unknown())
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('Present ONLY when this poll found nothing new but the human HAS spoken this session: everything they said, including what was already handed to you inline on an earlier tool result. An empty `messages` never means silence.'),
|
|
102
|
+
},
|
|
97
103
|
handler: (deps, args) => {
|
|
98
104
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
105
|
+
const messages = session.drainInbox();
|
|
106
|
+
if (messages.length > 0)
|
|
107
|
+
return Promise.resolve({ messages });
|
|
108
|
+
// Nothing NEW is not the same as nothing said. The inbox has two consumers — this poll and the
|
|
109
|
+
// control envelope spliced onto every tool result — and delivery is destructive, so a message
|
|
110
|
+
// handed over inline moments ago left this call reporting an empty queue with a note that read
|
|
111
|
+
// as "the human has said nothing". Reported from the field, from both ends: the agent believed
|
|
112
|
+
// it, and the person who typed the message got silence with no sign it had been seen.
|
|
113
|
+
const history = session.inboxHistory();
|
|
114
|
+
if (history.length > 0) {
|
|
115
|
+
return Promise.resolve({
|
|
116
|
+
messages,
|
|
117
|
+
delivered: [...history],
|
|
118
|
+
note: `nothing NEW since your last poll — but the human has sent ` +
|
|
119
|
+
`${String(history.length)} message(s) this session, listed in \`delivered\`. They were ` +
|
|
120
|
+
`handed to you inline on an earlier tool result, which is why this queue is empty. An ` +
|
|
121
|
+
`empty \`messages\` never means the human has said nothing.`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
99
124
|
// An empty inbox and a panel that is not wired both return `[]`. The first means the human has
|
|
100
125
|
// said nothing; the second means an agent is waiting on a channel that does not exist.
|
|
101
|
-
return Promise.resolve(noteEmptyRead({ messages
|
|
126
|
+
return Promise.resolve(noteEmptyRead({ messages }, 'messages', {
|
|
102
127
|
noun: 'messages from the human since the last poll',
|
|
103
128
|
}));
|
|
104
129
|
},
|
|
@@ -126,16 +151,26 @@ export const LIVE_CONTROL_TOOLS = [
|
|
|
126
151
|
marks: z.array(z.unknown()),
|
|
127
152
|
pendingCount: z.number(),
|
|
128
153
|
resolved: z.boolean().optional(),
|
|
154
|
+
resolvedNote: z
|
|
155
|
+
.string()
|
|
156
|
+
.optional()
|
|
157
|
+
.describe('The note on the mark that was actually retired. CHECK IT against the mark you meant to resolve: a bare `resolved:true` cannot tell you it closed the right one, and ids are only meaningful while the session that issued them lives.'),
|
|
129
158
|
},
|
|
130
159
|
handler: (deps, args) => {
|
|
131
160
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
132
161
|
const resolveId = asString(args['resolve']);
|
|
133
162
|
let resolved;
|
|
163
|
+
let resolvedNote;
|
|
134
164
|
if (resolveId !== undefined) {
|
|
135
165
|
// Grab the note BEFORE retiring it so we can close the loop visually for the human.
|
|
136
166
|
const mark = session.allMarks().find((m) => m.id === resolveId);
|
|
137
167
|
resolved = session.resolveMark(resolveId);
|
|
138
168
|
if (resolved && mark !== undefined) {
|
|
169
|
+
// Echoed back to the CALLER too, not only to the panel. A bare `resolved:true` is equally
|
|
170
|
+
// consistent with "I retired the bug you fixed" and "I retired somebody else's", and an
|
|
171
|
+
// agent hit exactly that — it closed a mark the human had recorded minutes later while
|
|
172
|
+
// the two it had actually fixed vanished with no record.
|
|
173
|
+
resolvedNote = mark.note;
|
|
139
174
|
// The human watching the panel sees their flagged bug get marked fixed (fire-and-forget).
|
|
140
175
|
session.pushNarration(`✓ fixed: ${mark.note}`);
|
|
141
176
|
}
|
|
@@ -148,6 +183,8 @@ export const LIVE_CONTROL_TOOLS = [
|
|
|
148
183
|
};
|
|
149
184
|
if (resolved !== undefined)
|
|
150
185
|
out.resolved = resolved;
|
|
186
|
+
if (resolvedNote !== undefined)
|
|
187
|
+
out.resolvedNote = resolvedNote;
|
|
151
188
|
return Promise.resolve(out);
|
|
152
189
|
},
|
|
153
190
|
},
|
|
@@ -4,15 +4,6 @@ export interface InboxMessage {
|
|
|
4
4
|
text: string;
|
|
5
5
|
t: number;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
8
|
-
* The human-in-the-loop half of a session: its lifecycle state, and the one-way inbox a person uses
|
|
9
|
-
* to talk to the agent driving their browser.
|
|
10
|
-
*
|
|
11
|
-
* Server-owned on purpose — a paused session has to stay paused even if the page reloads, so this
|
|
12
|
-
* cannot live in the tab. Split out of Session because it shares nothing with the transport, the
|
|
13
|
-
* ring buffer, or the journal: it is state a HUMAN changes, on a human's timescale, while everything
|
|
14
|
-
* else in Session is driven by the event stream.
|
|
15
|
-
*/
|
|
16
7
|
export declare class LiveControl {
|
|
17
8
|
#private;
|
|
18
9
|
state(): SessionState;
|
|
@@ -22,6 +13,13 @@ export declare class LiveControl {
|
|
|
22
13
|
setState(next: SessionState): void;
|
|
23
14
|
/** Queue a human note. Empty/whitespace-only text is ignored so a stray enter cannot spam the agent. */
|
|
24
15
|
push(text: string, t: number): void;
|
|
16
|
+
/**
|
|
17
|
+
* Everything the human has said this session, including what has already been delivered.
|
|
18
|
+
*
|
|
19
|
+
* Read-only and non-destructive by design — this is the answer to "did the human say anything
|
|
20
|
+
* while I was away", which a destructive drain can never give twice.
|
|
21
|
+
*/
|
|
22
|
+
history(): readonly InboxMessage[];
|
|
25
23
|
/** Return the queued notes AND clear them — delivered once, so a hint never repeats. */
|
|
26
24
|
drain(): InboxMessage[];
|
|
27
25
|
/** Diagnostic read of the inbox depth (does not clear). */
|
|
@@ -8,9 +8,29 @@ import { HumanControlKind, SessionState } from '@reticlehq/core';
|
|
|
8
8
|
* ring buffer, or the journal: it is state a HUMAN changes, on a human's timescale, while everything
|
|
9
9
|
* else in Session is driven by the event stream.
|
|
10
10
|
*/
|
|
11
|
+
/**
|
|
12
|
+
* How many delivered messages a session remembers.
|
|
13
|
+
*
|
|
14
|
+
* Enough that an agent picking up a long session can see what was said; bounded because a session
|
|
15
|
+
* can run for hours and this is a convenience, not a record.
|
|
16
|
+
*/
|
|
17
|
+
const HISTORY_CAP = 200;
|
|
11
18
|
export class LiveControl {
|
|
12
19
|
#state = SessionState.ACTIVE;
|
|
13
20
|
#inbox = [];
|
|
21
|
+
/**
|
|
22
|
+
* Everything the human has said this session, delivered or not.
|
|
23
|
+
*
|
|
24
|
+
* The inbox has TWO consumers — the control envelope on every tool result, and the explicit
|
|
25
|
+
* message poll — and `drain` is destructive, so whichever ran first took the message and the other
|
|
26
|
+
* reported an empty queue. That produced a confident "no messages from the human since the last
|
|
27
|
+
* poll" about a message the human had definitely sent, with no way for the agent to tell the
|
|
28
|
+
* difference and no acknowledgement for the person who typed it.
|
|
29
|
+
*
|
|
30
|
+
* Delivered-once stays: a guidance hint that repeats every turn is noise. This only stops the
|
|
31
|
+
* delivery from being the same event as the forgetting.
|
|
32
|
+
*/
|
|
33
|
+
#history = [];
|
|
14
34
|
state() {
|
|
15
35
|
return this.#state;
|
|
16
36
|
}
|
|
@@ -29,7 +49,20 @@ export class LiveControl {
|
|
|
29
49
|
const trimmed = text.trim();
|
|
30
50
|
if (0 === trimmed.length)
|
|
31
51
|
return;
|
|
32
|
-
|
|
52
|
+
const message = { text: trimmed, t };
|
|
53
|
+
this.#inbox.push(message);
|
|
54
|
+
this.#history.push(message);
|
|
55
|
+
if (this.#history.length > HISTORY_CAP)
|
|
56
|
+
this.#history.splice(0, this.#history.length - HISTORY_CAP);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Everything the human has said this session, including what has already been delivered.
|
|
60
|
+
*
|
|
61
|
+
* Read-only and non-destructive by design — this is the answer to "did the human say anything
|
|
62
|
+
* while I was away", which a destructive drain can never give twice.
|
|
63
|
+
*/
|
|
64
|
+
history() {
|
|
65
|
+
return this.#history;
|
|
33
66
|
}
|
|
34
67
|
/** Return the queued notes AND clear them — delivered once, so a hint never repeats. */
|
|
35
68
|
drain() {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Turning "no browser session connected" from a dead end into a next action.
|
|
3
3
|
*
|
|
4
|
-
* This is the most consequential sentence in the product.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* This is the most consequential sentence in the product. Most sessions never call a Reticle tool at
|
|
5
|
+
* all, and of the ones that do, a large share make exactly ONE call — usually `reticle_sessions` —
|
|
6
|
+
* and stop. Almost none of those ever touched a browser, and this is the session error they hit. The
|
|
7
|
+
* agent asks whether anything is connected, is told no, and leaves.
|
|
8
8
|
*
|
|
9
9
|
* The old message asked the agent to check two things it cannot see from where it stands ("is your
|
|
10
10
|
* app running with the SDK enabled?", "does it point at this port?"). The daemon can actually tell
|
|
@@ -27,6 +27,14 @@ interface NoSessionFacts {
|
|
|
27
27
|
listening: readonly number[];
|
|
28
28
|
/** The port this daemon is on — half of the mismatch the old message asked about. */
|
|
29
29
|
port: number;
|
|
30
|
+
/**
|
|
31
|
+
* The directory `initialized` was decided in, named in the message when it is known.
|
|
32
|
+
*
|
|
33
|
+
* "There is no `.reticle.json`" is only ever true OF A DIRECTORY, and a reader who cannot see
|
|
34
|
+
* which one has to guess whether the claim is about their app at all — reported by someone whose
|
|
35
|
+
* app lives in `src/admin` while the daemon stands at the repo root.
|
|
36
|
+
*/
|
|
37
|
+
directory?: string;
|
|
30
38
|
/**
|
|
31
39
|
* This daemon has reaped at least one EXPIRED pooled lease.
|
|
32
40
|
*
|
|
@@ -34,6 +42,17 @@ interface NoSessionFacts {
|
|
|
34
42
|
* evidence, not proof, and the message below is worded accordingly.
|
|
35
43
|
*/
|
|
36
44
|
leaseExpired?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The port `.reticle.json` configures for this project, when there is one.
|
|
47
|
+
*
|
|
48
|
+
* Half of a mismatch the daemon can settle rather than hint at: it knows the port it bound, and
|
|
49
|
+
* this is the number the app's SDK was built to dial. `doctor` already compares them and prints a
|
|
50
|
+
* precise line; the agent reading `why` was getting "check that the port matches" instead.
|
|
51
|
+
*
|
|
52
|
+
* Absent means no config, or one without a port, which is NOT a mismatch: a plugin-wired app has
|
|
53
|
+
* no `.reticle.json` at all and is working perfectly.
|
|
54
|
+
*/
|
|
55
|
+
projectPort?: number;
|
|
37
56
|
}
|
|
38
57
|
/**
|
|
39
58
|
* Every branch ends with this, and `recoveryFor` keys on it to suppress the generic no-session
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Turning "no browser session connected" from a dead end into a next action.
|
|
3
3
|
*
|
|
4
|
-
* This is the most consequential sentence in the product.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* This is the most consequential sentence in the product. Most sessions never call a Reticle tool at
|
|
5
|
+
* all, and of the ones that do, a large share make exactly ONE call — usually `reticle_sessions` —
|
|
6
|
+
* and stop. Almost none of those ever touched a browser, and this is the session error they hit. The
|
|
7
|
+
* agent asks whether anything is connected, is told no, and leaves.
|
|
8
8
|
*
|
|
9
9
|
* The old message asked the agent to check two things it cannot see from where it stands ("is your
|
|
10
10
|
* app running with the SDK enabled?", "does it point at this port?"). The daemon can actually tell
|
|
@@ -30,12 +30,12 @@ const RETRY = SELF_RECOVERING_MARKER;
|
|
|
30
30
|
* The way out that needs no human at all.
|
|
31
31
|
*
|
|
32
32
|
* `reticle_lease` opens a browser Reticle drives itself, instead of waiting for somebody's tab to
|
|
33
|
-
* dial in.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
33
|
+
* dial in. In the field it is the single strongest predictor of a session that works: sessions that
|
|
34
|
+
* use it drive an order of magnitude more tool calls than those that do not, they account for a
|
|
35
|
+
* disproportionate share of every bug found, and no single-call bounce has ever used one. It is also
|
|
36
|
+
* advertised on no profile except `full`, so an agent only ever finds it if it already knew it
|
|
37
|
+
* existed. Naming it HERE puts it in front of the agent at the one moment it is the answer, and
|
|
38
|
+
* costs nothing on the turns when it is not.
|
|
39
39
|
*
|
|
40
40
|
* Only offered when the app is known to carry the SDK: leasing an uninstrumented app just burns a
|
|
41
41
|
* browser and comes back `ready:false`.
|
|
@@ -51,19 +51,75 @@ const SELF_SERVE = 'You do not have to wait for the human: reticle_lease {action
|
|
|
51
51
|
* evidence that was never gathered.
|
|
52
52
|
*/
|
|
53
53
|
/**
|
|
54
|
-
* The
|
|
54
|
+
* The one cause that produces a PERFECTLY healthy everything and still never connects.
|
|
55
55
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* The SDK refuses to dial from a page that is not on localhost unless `allowNonLocalhost` is set.
|
|
57
|
+
* That is deliberate — Reticle must not be reachable from a page it does not trust — but the refusal
|
|
58
|
+
* happens page-side, so the daemon sees only silence, `doctor` sees only a healthy daemon, and every
|
|
59
|
+
* checklist item passes. Reported from the field by an agent on a hosts-file alias (the normal setup
|
|
60
|
+
* for white-label and multi-tenant apps): the whole setup was lost to a flag named in no checklist,
|
|
61
|
+
* and `init`'s own fallback snippet guards on `hostname === 'localhost'`, so on such a host the
|
|
62
|
+
* connect never runs and there is not even a console line to find.
|
|
63
|
+
*
|
|
64
|
+
* Only offered on the wired-and-listening branch: that is where a correctly installed app that
|
|
65
|
+
* cannot connect actually lands.
|
|
59
66
|
*/
|
|
60
|
-
const
|
|
61
|
-
'
|
|
62
|
-
|
|
67
|
+
const NON_LOCALHOST_GATE = 'One more cause that leaves every other check healthy: if the app is served on anything other ' +
|
|
68
|
+
'than localhost (a hosts-file alias, a LAN IP, a tunnel), the SDK refuses to connect unless it ' +
|
|
69
|
+
'is given `allowNonLocalhost: true` — the refusal is page-side, so nothing here can see it. ' +
|
|
70
|
+
"Check the browser console for that message and pass the flag in the app's reticle.connect().";
|
|
63
71
|
const SCANNED_PORTS = [...DEV_SERVER_PORTS].join(', ');
|
|
72
|
+
/**
|
|
73
|
+
* The commonest first-run state there is, and until #320 the message named it nowhere.
|
|
74
|
+
*
|
|
75
|
+
* Reticle sees a page only once a browser has LOADED it. A wired app that nobody has opened produces
|
|
76
|
+
* exactly the same empty list as a broken install, and the reporter who hit it spent an afternoon
|
|
77
|
+
* re-verifying their init output, diffing their Vite config and curling their own page before
|
|
78
|
+
* discovering that one `reticle open` fixed it instantly.
|
|
79
|
+
*/
|
|
80
|
+
const OPEN_THE_APP = 'Reticle only ever sees a page that is LOADED, so the commonest cause by a distance is that no ' +
|
|
81
|
+
"browser has opened the app yet: run `reticle open <url>` with the app's own URL (or ask the " +
|
|
82
|
+
'human to open it).';
|
|
83
|
+
/**
|
|
84
|
+
* What a machine-wide port scan can and cannot say.
|
|
85
|
+
*
|
|
86
|
+
* It finds listeners anywhere on localhost and knows nothing about who owns them. The old wording
|
|
87
|
+
* spent that as evidence — "something IS listening on port 5173, 8000, 8080, SO a server is up and
|
|
88
|
+
* has never dialled this daemon" — about three ports that belonged to three other repositories on
|
|
89
|
+
* the reporter's machine, while their own app sat on a port the scan does not cover. On any machine
|
|
90
|
+
* running more than one instrumented repo, which is the normal case here, that inference is unsound.
|
|
91
|
+
*/
|
|
92
|
+
function unattributedListeners(listening) {
|
|
93
|
+
if (0 === listening.length) {
|
|
94
|
+
return (`No listener found that I can attribute to this project: the scan covers ${SCANNED_PORTS} ` +
|
|
95
|
+
'across the whole machine, so an app on any other port is invisible to it.');
|
|
96
|
+
}
|
|
97
|
+
return (`Something is listening on port ${listening.join(', ')}, but I cannot attribute any of it to ` +
|
|
98
|
+
`this project: that is a machine-wide scan of ${SCANNED_PORTS}, so on a machine running more ` +
|
|
99
|
+
"than one repo those are as likely to be somebody else's dev server, and the app's own port " +
|
|
100
|
+
'may not be in the set at all. Treat them as unattributed rather than as evidence.');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The mismatch, stated with both numbers, when we can actually see one. Empty otherwise.
|
|
104
|
+
*
|
|
105
|
+
* Leading space so the caller can concatenate it unconditionally without producing a double space
|
|
106
|
+
* in the common case where there is nothing to say.
|
|
107
|
+
*/
|
|
108
|
+
function portMismatchClause(facts) {
|
|
109
|
+
const configured = facts.projectPort;
|
|
110
|
+
if (configured === undefined || configured === facts.port)
|
|
111
|
+
return '';
|
|
112
|
+
return (` They already disagree: \`.reticle.json\` says port ${String(configured)} and this daemon is` +
|
|
113
|
+
` on ${String(facts.port)}, so the SDK is dialling ${String(configured)} and nothing is there.` +
|
|
114
|
+
` Either restart the daemon on ${String(configured)}, or update \`.reticle.json\` to` +
|
|
115
|
+
` ${String(facts.port)} and restart the dev server so the app picks it up.`);
|
|
116
|
+
}
|
|
64
117
|
export function diagnoseNoSession(facts) {
|
|
65
118
|
const { everConnected, initialized, listening, port } = facts;
|
|
66
|
-
|
|
119
|
+
// Named when known: a claim about a missing file is a claim about ONE directory.
|
|
120
|
+
const where = facts.directory === undefined
|
|
121
|
+
? 'the directory this daemon is running in'
|
|
122
|
+
: `the directory this daemon is running in (${facts.directory})`;
|
|
67
123
|
if (everConnected) {
|
|
68
124
|
// A reaped lease first, because it is the one cause we have POSITIVE evidence for. Reported
|
|
69
125
|
// from the field (#157): an aged-out lease produced "the tab was closed … ask the human to
|
|
@@ -72,49 +128,77 @@ export function diagnoseNoSession(facts) {
|
|
|
72
128
|
// went looking for a port mismatch. Hedged rather than swapped: a human tab may ALSO have
|
|
73
129
|
// closed, and this does not know which session went.
|
|
74
130
|
if (true === facts.leaseExpired) {
|
|
75
|
-
return ('no browser session connected
|
|
131
|
+
return ('no browser session connected, but one WAS connected to this daemon earlier, so the wiring ' +
|
|
76
132
|
'is correct. This daemon has expired at least one pooled lease, so the likeliest cause is ' +
|
|
77
133
|
'that a lease you were using aged out; a lease is a headless context, not a human tab, and ' +
|
|
78
134
|
'it takes its cookies with it (so an authenticated app needs signing in again). Re-acquire ' +
|
|
79
135
|
'with reticle_lease {action:"acquire", url} and carry on. If you were driving a human tab ' +
|
|
80
136
|
`instead, it went away — reopen it or run \`reticle open\`. ${RETRY}`);
|
|
81
137
|
}
|
|
82
|
-
return ('no browser session connected
|
|
138
|
+
return ('no browser session connected, but one WAS connected to this daemon earlier, so the wiring ' +
|
|
83
139
|
'is correct. The tab was closed, navigated away, or hard-reloaded. Ask the human to reopen ' +
|
|
84
140
|
`the app (or run \`reticle open\`), or reload the tab. ${SELF_SERVE} ${RETRY}`);
|
|
85
141
|
}
|
|
86
142
|
if (0 === listening.length) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
143
|
+
// Lead with the stronger EVIDENCE, and do not overstate what it is.
|
|
144
|
+
//
|
|
145
|
+
// `initialized` is one thing only: whether a `.reticle.json` sits in the directory this daemon
|
|
146
|
+
// is running in. It is NOT "this project has never been through `reticle init`", and the two
|
|
147
|
+
// come apart constantly — a monorepo whose daemon runs at the root while the app lives in
|
|
148
|
+
// `apps/web`, or any app wired by the babel/vite plugin rather than by `init`. An earlier
|
|
149
|
+
// version of this branch asserted the strong claim and led with it, on the reasoning that it was
|
|
150
|
+
// a certainty while the port scan was a guess. It is not a certainty. Caught driving Reticle's
|
|
151
|
+
// OWN repo, where the message told an agent the project had never been through `init` about a
|
|
152
|
+
// fixture that is instrumented and working — "the one sentence I was most likely to act on was
|
|
153
|
+
// wrong", which is the exact failure this whole file exists to prevent.
|
|
154
|
+
//
|
|
155
|
+
// So: report what was actually checked, name the directory, and name the case where the absence
|
|
156
|
+
// is expected rather than diagnostic.
|
|
157
|
+
if (!initialized) {
|
|
158
|
+
return ('no browser session connected. Two things to weigh, and neither of them is proof. ' +
|
|
159
|
+
`(1) Nothing is listening on the ports Reticle scans (${SCANNED_PORTS}), so the dev server ` +
|
|
160
|
+
'may not be running — ask the human to start it (`npm run dev`). That scan is narrow ' +
|
|
161
|
+
'though: a server on any other port is invisible to it, so if the app IS running, ask for ' +
|
|
162
|
+
'its URL rather than assuming it is down, and open it with `reticle open <url>`. ' +
|
|
163
|
+
`(2) There is no \`.reticle.json\` in ${where}. That is the ` +
|
|
164
|
+
"file `reticle init` writes, so the app may carry no Reticle SDK — but check the app's " +
|
|
165
|
+
'OWN directory before re-running `init`: in a monorepo the daemon often runs at the root ' +
|
|
166
|
+
'while the app lives in a subdirectory, and an app wired by the Vite or Babel plugin ' +
|
|
167
|
+
`carries the SDK without that file at all. ${RETRY}`);
|
|
168
|
+
}
|
|
169
|
+
return (`no browser session connected, and this daemon has never seen one. ${OPEN_THE_APP} ` +
|
|
170
|
+
'Nothing is listening on the ports Reticle scans ' +
|
|
171
|
+
`(${SCANNED_PORTS}) either, and the most common reason for that is a dev server that is not ` +
|
|
172
|
+
'running: ask the human to start it (`npm run dev`), then open the app in a browser. ' +
|
|
90
173
|
// The caveat is here rather than omitted because the scan is NARROW, and the old sentence
|
|
91
|
-
// spent its confidence as though an empty result
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
174
|
+
// spent its confidence as though an empty result were proof of absence. Reported twice: a
|
|
175
|
+
// scripted drive of 2.5.0 asserted the app was not running while it served 200 on :7699, and
|
|
176
|
+
// an agent was told nothing was listening while a dev server answered on :5000 under a custom
|
|
177
|
+
// hostname. The common defaults have since been added to the scanned set, which narrows the
|
|
178
|
+
// gap and cannot close it — anything passed to `--port` is still invisible.
|
|
95
179
|
'That scan is narrow, so it is not proof: a server on any other port is invisible to it. If ' +
|
|
96
180
|
// Deliberately NOT offering reticle_lease here, and a test pins that: a lease opens a URL, and
|
|
97
181
|
// if nothing is listening there is nothing at any URL to open. Asking for the real one is the
|
|
98
182
|
// only move that can recover the :7699 case.
|
|
99
183
|
'the app IS running, ask the human for its URL rather than assuming it is down. ' +
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
// SDK. Two round trips to learn two things the daemon knew on the first call, and this is the
|
|
104
|
-
// largest cohort in the funnel (#171): 77 users attached an agent and never instrumented an
|
|
105
|
-
// app. They have a daemon (they registered the MCP server) and no SDK anywhere.
|
|
106
|
-
`${initialized ? '' : UNINSTRUMENTED} ${RETRY}`);
|
|
184
|
+
// Reachable only for a project that HAS been through `init` — the uninstrumented case is
|
|
185
|
+
// answered above, leading with that certainty instead of behind this scan's guess.
|
|
186
|
+
`${RETRY}`);
|
|
107
187
|
}
|
|
108
188
|
if (!initialized) {
|
|
109
|
-
return (
|
|
110
|
-
|
|
111
|
-
'
|
|
112
|
-
|
|
113
|
-
|
|
189
|
+
return ('no browser session connected, and this daemon has never seen one. ' +
|
|
190
|
+
`What was actually checked: there is no \`.reticle.json\` in ${where}. That is the file ` +
|
|
191
|
+
'`reticle init` writes, so the app may carry no Reticle SDK — but it is not proof, and the ' +
|
|
192
|
+
'same absence is expected in a monorepo whose daemon runs at the root while the app lives in ' +
|
|
193
|
+
"a subdirectory, or in an app wired by the Vite or Babel plugin. Check the app's OWN " +
|
|
194
|
+
'directory: if it has no config, run `reticle init` there and restart the dev server; if it ' +
|
|
195
|
+
'has one, the app is wired and simply has no page open — `reticle open <url>`. ' +
|
|
196
|
+
`${unattributedListeners(listening)} ${RETRY}`);
|
|
114
197
|
}
|
|
115
|
-
return (
|
|
116
|
-
`wired for Reticle
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
198
|
+
return ('no browser session connected, and this daemon has never seen one for this project, which is ' +
|
|
199
|
+
`wired for Reticle. ${OPEN_THE_APP} ${unattributedListeners(listening)} ` +
|
|
200
|
+
'If the page IS open and still does not appear, the app is either serving a build made before ' +
|
|
201
|
+
`the wiring landed or dialling a different daemon than this one (on ${String(port)}): restart ` +
|
|
202
|
+
"the dev server, hard-reload the page, and check that the app's reticle port matches " +
|
|
203
|
+
`${String(port)}.${portMismatchClause(facts)} ${NON_LOCALHOST_GATE} ${SELF_SERVE} ${RETRY}`);
|
|
120
204
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { probeDevServers } from './dev-server-probe.js';
|
|
13
13
|
import { diagnoseNoSession } from './no-session-diagnosis.js';
|
|
14
|
+
import { readProjectPort } from '../cli/cli-port.js';
|
|
14
15
|
/** Slow enough to be free, fast enough that a dev server started 15s ago is already reflected. */
|
|
15
16
|
const REFRESH_MS = 15_000;
|
|
16
17
|
/** Start the watch. Returns a stop function; the timer is unref'd so it never holds the daemon up. */
|
|
@@ -42,7 +43,18 @@ function startNoSessionWatch(options) {
|
|
|
42
43
|
initialized: options.initialized,
|
|
43
44
|
listening,
|
|
44
45
|
port: options.port,
|
|
46
|
+
// The directory `initialized` was decided in. Named in the message because "there is no
|
|
47
|
+
// `.reticle.json`" is a claim about ONE directory, and a reader standing somewhere else
|
|
48
|
+
// cannot tell whether it is a claim about their app at all.
|
|
49
|
+
directory: options.directory ?? process.cwd(),
|
|
45
50
|
leaseExpired: (options.reapedLeases?.() ?? 0) > 0,
|
|
51
|
+
// Read here rather than at boot: `.reticle.json` can be written by `init` after this daemon
|
|
52
|
+
// started, which is the ordinary first-install order, and a port cached from before it existed
|
|
53
|
+
// would make the daemon confidently report no mismatch on the one run where there is one.
|
|
54
|
+
...(() => {
|
|
55
|
+
const configured = readProjectPort(options.directory ?? process.cwd());
|
|
56
|
+
return configured === undefined ? {} : { projectPort: configured };
|
|
57
|
+
})(),
|
|
46
58
|
}));
|
|
47
59
|
return () => {
|
|
48
60
|
clearInterval(timer);
|