@reticlehq/server 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +16 -13
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/cli-doctor.js +25 -5
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +7 -1
- package/dist/command/cli/cli-parse.js +33 -5
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +129 -17
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +1 -1
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/launch/cli-launch.d.ts +1 -29
- package/dist/command/cli/launch/cli-launch.js +6 -40
- package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
- package/dist/command/cli/ports/resolve/cli-port.js +45 -1
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli.js +5 -0
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +1 -0
- package/dist/features/harness/jev-driver.js +6 -9
- package/dist/features/harness/platform-config.d.ts +10 -0
- package/dist/features/harness/platform-config.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +40 -95
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +31 -9
- package/dist/language/flows/flow-replay-run.js +146 -33
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +12 -11
- package/dist/language/flows/flow-replay.js +79 -109
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flows.d.ts +8 -4
- package/dist/language/flows/flows.js +11 -2
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +12 -8
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/verify-change-tools.js +14 -0
- package/dist/memory/cloud/cloud-sync.d.ts +0 -2
- package/dist/memory/cloud/cloud-sync.js +1 -1
- package/dist/memory/cloud/harness-config.d.ts +2 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +13 -0
- package/dist/memory/cloud/sync-daemon.js +21 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-store.js +6 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +252 -140
- package/dist/memory/intent/intent-shard.d.ts +10 -11
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-recorder.d.ts +21 -0
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +14 -10
- package/dist/memory/journal/on-disk/retention.js +13 -12
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/session-end.d.ts +4 -1
- package/dist/memory/journal/session-end.js +64 -6
- package/dist/memory/journal/session-journal.d.ts +8 -1
- package/dist/memory/journal/session-journal.js +10 -0
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
- package/dist/memory/project/fs/fs-port.js +44 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.js +5 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +9 -0
- package/dist/portal/session/session-manager.js +23 -2
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +19 -7
- package/dist/surface/mcp/mcp-proxy.js +3 -3
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +22 -7
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +30 -4
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/dynamic-tools.js +6 -0
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +17 -4
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/observe-tools.js +14 -6
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +5 -0
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools.js +64 -29
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/cli/cloud.mdx +9 -7
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/desktop.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +128 -4
- package/docs/tools/query.mdx +10 -10
- package/docs/usage.md +1 -1
- package/docs/what-is-recorded.md +62 -0
- package/package.json +6 -6
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -15,8 +15,6 @@ import { type NetworkMutationPort } from '../../portal/input/network-mutation.js
|
|
|
15
15
|
import type { Perturbation } from '@reticlehq/core';
|
|
16
16
|
import { type SeedStorage } from '@reticlehq/core';
|
|
17
17
|
import type { ToolDef } from './tool-kit.js';
|
|
18
|
-
/** The origin of a URL, or undefined when it does not parse. */
|
|
19
|
-
export declare function originOf(url: string): string | undefined;
|
|
20
18
|
/**
|
|
21
19
|
* Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
|
|
22
20
|
* adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
|
|
@@ -78,31 +76,6 @@ export declare function resolveLeasedSessionId(sessions: {
|
|
|
78
76
|
* a human's own URL.
|
|
79
77
|
*/
|
|
80
78
|
export declare function carryReticleIdentity(fromUrl: string | undefined, toUrl: string): string;
|
|
81
|
-
/** The narrow slice of a session the probe needs. Anything that quacks like this works. */
|
|
82
|
-
interface ProbeableSession {
|
|
83
|
-
command?: (name: string, args: Record<string, unknown>, timeoutMs: number) => Promise<unknown>;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Does this tab still answer?
|
|
87
|
-
*
|
|
88
|
-
* PRESENCE IS NOT LIVENESS. The sessions map still holds a tab that is attached, streaming events,
|
|
89
|
-
* and answering nothing, so `ready` read off a row in that map hands back a lease that `snapshot`,
|
|
90
|
-
* `state` and `console` then reject (#692). It has to mean the tab replied.
|
|
91
|
-
*
|
|
92
|
-
* ANY reply counts, including one that reports the command failed. The question is whether the SDK
|
|
93
|
-
* answers at all, not what it says — so an SDK too old to know the command replies
|
|
94
|
-
* `unknown command '…'`, and that is proof. Only the absence of a reply is evidence of absence:
|
|
95
|
-
* `PendingCommands.track` REJECTS on timeout and on a dropped socket, and resolves on every real
|
|
96
|
-
* answer, so the two cases are already separated for us.
|
|
97
|
-
*
|
|
98
|
-
* `CAPABILITIES` is the probe because it takes no arguments and returns a small fixed list. No new
|
|
99
|
-
* wire command is introduced: this rides an existing round trip.
|
|
100
|
-
*
|
|
101
|
-
* Fails OPEN. A registry entry with no `command` is a shape this code did not put there, and
|
|
102
|
-
* turning a lease that works into a refusal over a probe that could not run would be a worse
|
|
103
|
-
* failure than the one being fixed.
|
|
104
|
-
*/
|
|
105
|
-
export declare function probeLeaseAlive(session: ProbeableSession | undefined, timeoutMs?: number): Promise<boolean>;
|
|
106
79
|
/**
|
|
107
80
|
* Poll until the leased tab's SDK has actually registered on the bridge, so the sessionId we return
|
|
108
81
|
* is immediately usable (the page navigates first, then connects a moment later — without this wait
|
|
@@ -122,6 +95,7 @@ export declare function acquireLeasedSession(pool: {
|
|
|
122
95
|
sessionId: string;
|
|
123
96
|
release: () => Promise<void>;
|
|
124
97
|
navStatus?: number;
|
|
98
|
+
injectReader?: () => Promise<boolean>;
|
|
125
99
|
}>;
|
|
126
100
|
/**
|
|
127
101
|
* The address this lease's page said it could not reach. Optional so a test double need not
|
|
@@ -141,6 +115,21 @@ export declare function acquireLeasedSession(pool: {
|
|
|
141
115
|
release: () => Promise<void>;
|
|
142
116
|
}>;
|
|
143
117
|
export declare function waitForLeasedSession(isConnected: () => boolean, sleeper?: (ms: number) => Promise<void>, attempts?: number, pollMs?: number): Promise<boolean>;
|
|
118
|
+
/**
|
|
119
|
+
* Wait for the leased tab to connect; if its app never does, hand it the zero-install reader.
|
|
120
|
+
*
|
|
121
|
+
* The decision is taken HERE, by the daemon, on the one fact that settles it: no HELLO arrived. A
|
|
122
|
+
* timer inside the page cannot know that — an app can connect from a dynamic import that resolves
|
|
123
|
+
* after `load` — and a reader injected into a page whose app was about to connect would take its
|
|
124
|
+
* place and drop the app's own options. So an app that connected itself is never touched, and a page
|
|
125
|
+
* that stayed silent through the whole readiness wait is the one that gets a reader.
|
|
126
|
+
*/
|
|
127
|
+
export declare function connectOrInject(lease: {
|
|
128
|
+
injectReader?: () => Promise<boolean>;
|
|
129
|
+
}, isConnected: () => boolean, wait?: (isConnected: () => boolean) => Promise<boolean>): Promise<{
|
|
130
|
+
ready: boolean;
|
|
131
|
+
zeroInstall: boolean;
|
|
132
|
+
}>;
|
|
144
133
|
export declare function hasOriginLock(origin: string): boolean;
|
|
145
134
|
export declare const LEASE_ACQUIRE_TOOL: ToolDef;
|
|
146
135
|
export declare const LEASE_TOOLS: ToolDef[];
|
|
@@ -192,4 +181,3 @@ export declare function suiteFixturePort(realInput: RealInputProvider | undefine
|
|
|
192
181
|
* is the reach the boundary guard exists to ask about.
|
|
193
182
|
*/
|
|
194
183
|
export declare function sessionMutationPort(realInput: RealInputProvider | undefined, appUrl: string | undefined): Promise<NetworkMutationPort | undefined>;
|
|
195
|
-
export {};
|
|
@@ -14,6 +14,8 @@ import { mutationPortFor } from '../../portal/input/network-mutation.js';
|
|
|
14
14
|
import { z } from 'zod';
|
|
15
15
|
import { leaseNotConnectedHint } from './lease-hint.js';
|
|
16
16
|
import { probeSdkMarker } from './gaps/sdk-marker-probe.js';
|
|
17
|
+
import { observeWebDocument } from '../../portal/session/dev-server/served-document.js';
|
|
18
|
+
import { diagnoseObservedWebCsp } from '@reticlehq/init';
|
|
17
19
|
import { readProjectFramework, readProjectId } from '../../command/cli/ports/resolve/cli-port.js';
|
|
18
20
|
import { hasAnyAppConnectedBefore } from '../../memory/recall/prior/connection-memory.js';
|
|
19
21
|
import { AGENT_DRIVING_ELSEWHERE, AGENT_DRIVING_HERE_AGAIN, watchersToNotify, } from '../../portal/session/lease-visibility.js';
|
|
@@ -36,14 +38,38 @@ import { chromiumHint } from '../../command/cli/doctor/browser/chromium-hint.js'
|
|
|
36
38
|
* to print a paragraph, and it can only ever add a sentence: the probe returns `undefined` on any
|
|
37
39
|
* doubt and the hint then says nothing about markers.
|
|
38
40
|
*/
|
|
41
|
+
/**
|
|
42
|
+
* Whether the served page's own CSP refuses the bridge's SOCKET, read by the same code `doctor` uses.
|
|
43
|
+
*
|
|
44
|
+
* Only `connect-src` counts here. A lease injects the SDK itself, so the inline-snippet problem a
|
|
45
|
+
* `script-src` finding describes does not apply; what stops a leased page is the socket. Said in the
|
|
46
|
+
* lease's own words, because doctor's advice is about wiring a project, not about a tab.
|
|
47
|
+
*/
|
|
48
|
+
async function observedCspBlock(url, port) {
|
|
49
|
+
const document = await observeWebDocument(url);
|
|
50
|
+
if (document === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
const refusesSocket = diagnoseObservedWebCsp(document, port).some((finding) => `${finding.problem} ${finding.fix}`.includes(CONNECT_SRC));
|
|
53
|
+
if (!refusesSocket)
|
|
54
|
+
return undefined;
|
|
55
|
+
const allow = `ws://localhost:${String(port)} ws://127.0.0.1:${String(port)}`;
|
|
56
|
+
return {
|
|
57
|
+
problem: `its ${CONNECT_SRC} does not allow ws://localhost:${String(port)}`,
|
|
58
|
+
fix: `add ${allow} to ${CONNECT_SRC}, behind a development-only check`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** The CSP directive that governs the socket the SDK dials. */
|
|
62
|
+
const CONNECT_SRC = 'connect-src';
|
|
39
63
|
async function leaseEvidence(deps, port, url) {
|
|
40
64
|
const cwd = process.cwd();
|
|
41
65
|
const projectId = readProjectId(cwd);
|
|
42
66
|
const refusal = deps.sessions.lastClosure()?.reason;
|
|
43
67
|
const framework = readProjectFramework(cwd);
|
|
44
68
|
const sdkMarker = await probeSdkMarker(url);
|
|
69
|
+
const cspBlock = await observedCspBlock(url, port);
|
|
45
70
|
return {
|
|
46
71
|
...(refusal === undefined ? {} : { refusal }),
|
|
72
|
+
...(cspBlock === undefined ? {} : { cspBlock }),
|
|
47
73
|
...(framework === undefined ? {} : { framework }),
|
|
48
74
|
...(sdkMarker === undefined ? {} : { sdkMarker }),
|
|
49
75
|
previouslyConnected: hasAnyAppConnectedBefore(reticleStateHome(), port, projectId),
|
|
@@ -52,7 +78,7 @@ async function leaseEvidence(deps, port, url) {
|
|
|
52
78
|
}
|
|
53
79
|
const POOL_UNAVAILABLE = 'browser pool unavailable — the lease tools need the daemon-managed pool (start Reticle via `reticle mcp`).';
|
|
54
80
|
/** The origin of a URL, or undefined when it does not parse. */
|
|
55
|
-
|
|
81
|
+
function originOf(url) {
|
|
56
82
|
try {
|
|
57
83
|
return new URL(url).origin;
|
|
58
84
|
}
|
|
@@ -298,7 +324,7 @@ const LEASE_PROBE_TIMEOUT_MS = 1_500;
|
|
|
298
324
|
* turning a lease that works into a refusal over a probe that could not run would be a worse
|
|
299
325
|
* failure than the one being fixed.
|
|
300
326
|
*/
|
|
301
|
-
|
|
327
|
+
async function probeLeaseAlive(session, timeoutMs = LEASE_PROBE_TIMEOUT_MS) {
|
|
302
328
|
const send = session?.command;
|
|
303
329
|
if (send === undefined)
|
|
304
330
|
return true;
|
|
@@ -338,7 +364,7 @@ export async function acquireLeasedSession(pool, sessions, url, projectId, seedS
|
|
|
338
364
|
// id comes back, so handing it the lease id for an app that named its own session sent every flow
|
|
339
365
|
// in the run at a session that does not exist.
|
|
340
366
|
let registeredId;
|
|
341
|
-
await
|
|
367
|
+
await connectOrInject(lease, () => {
|
|
342
368
|
registeredId = resolveLeasedSessionId(sessions, lease.sessionId);
|
|
343
369
|
return registeredId !== undefined;
|
|
344
370
|
});
|
|
@@ -364,6 +390,23 @@ export async function waitForLeasedSession(isConnected, sleeper = sleep, attempt
|
|
|
364
390
|
}
|
|
365
391
|
return isConnected();
|
|
366
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Wait for the leased tab to connect; if its app never does, hand it the zero-install reader.
|
|
395
|
+
*
|
|
396
|
+
* The decision is taken HERE, by the daemon, on the one fact that settles it: no HELLO arrived. A
|
|
397
|
+
* timer inside the page cannot know that — an app can connect from a dynamic import that resolves
|
|
398
|
+
* after `load` — and a reader injected into a page whose app was about to connect would take its
|
|
399
|
+
* place and drop the app's own options. So an app that connected itself is never touched, and a page
|
|
400
|
+
* that stayed silent through the whole readiness wait is the one that gets a reader.
|
|
401
|
+
*/
|
|
402
|
+
export async function connectOrInject(lease, isConnected, wait = waitForLeasedSession) {
|
|
403
|
+
if (await wait(isConnected))
|
|
404
|
+
return { ready: true, zeroInstall: false };
|
|
405
|
+
if (true !== (await lease.injectReader?.()))
|
|
406
|
+
return { ready: false, zeroInstall: false };
|
|
407
|
+
const ready = await wait(isConnected);
|
|
408
|
+
return { ready, zeroInstall: ready };
|
|
409
|
+
}
|
|
367
410
|
/**
|
|
368
411
|
* Named on its own because `reticle drive <url>` runs it too.
|
|
369
412
|
*
|
|
@@ -381,7 +424,7 @@ export function hasOriginLock(origin) {
|
|
|
381
424
|
}
|
|
382
425
|
export const LEASE_ACQUIRE_TOOL = {
|
|
383
426
|
name: ReticleTool.LEASE_ACQUIRE,
|
|
384
|
-
description: 'Lease a fresh isolated headless browser context from the shared pool and navigate it to the app URL (the app must already be running
|
|
427
|
+
description: 'Lease a fresh isolated headless browser context from the shared pool and navigate it to the app URL (the app must already be running; one with no Reticle SDK gets one supplied, reported as zeroInstall). If this origin is already leased and still connected, this returns THAT session rather than minting a second tab — a second acquire on the same origin poisons default session resolution. Returns the sessionId the leased tab registers — pass it to other tools. The pool keeps all leases in ONE browser and caps concurrency; if at capacity this waits for a free slot. Release with reticle_lease{action:"release"} when the flow is done. PREFER AN ALREADY-OPEN TAB: if reticle_sessions lists a non-leased session for this app, drive THAT instead — a lease is invisible to the person watching the app, whose HUD lives in their own tab, and a tab flagged hidden/throttled is often still driveable. Lease for isolation you actually need (a second identity, a clean context, parallel flows) or when driving the open tab has failed — this call answers with `preferExisting` when a live tab was available.',
|
|
385
428
|
inputSchema: {
|
|
386
429
|
url: z
|
|
387
430
|
.string()
|
|
@@ -398,6 +441,10 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
398
441
|
ready: z
|
|
399
442
|
.boolean()
|
|
400
443
|
.describe('Whether the leased tab is connected AND answering. On a reused lease this is probed, not assumed — a row in the session map is presence, not liveness. False ⇒ read notReadyReason.'),
|
|
444
|
+
zeroInstall: z
|
|
445
|
+
.boolean()
|
|
446
|
+
.optional()
|
|
447
|
+
.describe('Present when this app ships no Reticle SDK and the lease supplied one. Verdicts work on the DOM, network, console and routes; there is no framework adapter, so no component state and no source file:line. Install with `npx @reticlehq/server init` for those.'),
|
|
401
448
|
notReadyReason: z
|
|
402
449
|
.enum([LeaseNotReadyReason.SDK_NEVER_DIALLED, LeaseNotReadyReason.SDK_STOPPED_ANSWERING])
|
|
403
450
|
.optional()
|
|
@@ -538,7 +585,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
538
585
|
// Resolved rather than assumed: an app that names its own session registers under that name,
|
|
539
586
|
// and the id we hand back has to be the one the agent can actually drive.
|
|
540
587
|
let registeredId;
|
|
541
|
-
const ready = await
|
|
588
|
+
const { ready, zeroInstall } = await connectOrInject(lease, () => {
|
|
542
589
|
registeredId = resolveLeasedSessionId(deps.sessions, lease.sessionId);
|
|
543
590
|
return registeredId !== undefined;
|
|
544
591
|
});
|
|
@@ -569,6 +616,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
569
616
|
// word: no SDK ever dialled in (look at the install) versus one dialled in and stopped
|
|
570
617
|
// answering (recover the tab). They want different next actions, so they get names.
|
|
571
618
|
...(ready ? {} : { notReadyReason: LeaseNotReadyReason.SDK_NEVER_DIALLED }),
|
|
619
|
+
...(zeroInstall ? { zeroInstall: true } : {}),
|
|
572
620
|
expiresInMs: pool.leaseTtlMs(),
|
|
573
621
|
leased: pool.activeCount(),
|
|
574
622
|
queued: pool.queuedCount(),
|
|
@@ -19,7 +19,7 @@ export const LINEAGE_TOOLS = [
|
|
|
19
19
|
{
|
|
20
20
|
name: ReticleTool.LINEAGE,
|
|
21
21
|
example: { path: 'user.profile.name' },
|
|
22
|
-
description: 'Trace BACKWARDS from a runtime value: which state change set it, which signal preceded
|
|
22
|
+
description: 'Trace BACKWARDS from a runtime value: which state change set it, which driven action it was observed under, which signal preceded it, and which request preceded that. Read-only — it draws no verdict and changes nothing. The state change is OBSERVED; every link above it is an INFERENCE and says so in its own text — a driven action is the window-tier stamp the SDK put on the event, a signal or request is a timing look-back. A candidate stamped with a DIFFERENT action is excluded; an unstamped one is kept. Where several candidates could still explain a link it names ALL of them rather than choosing, because timing alone cannot justify the choice — so a chain here is a place to look, never a proven cause. Use it after a `no` verdict or a contradiction, instead of correlating reticle_look { action: "state" }, reticle_observe { action: "network" } and reticle_observe by hand.',
|
|
23
23
|
inputSchema: {
|
|
24
24
|
path: z
|
|
25
25
|
.string()
|
|
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
|
|
7
7
|
import { aliasParam } from './args/alias-args.js';
|
|
8
8
|
import { CONSOLE_ATTACH_NOTE, CONSOLE_LEVELS, ReticleCommand, DEFAULT_ASSERT_TIMEOUT_MS, PredicateKind, Verified, } from '@reticlehq/core';
|
|
9
9
|
import { ReticleTool } from '@reticlehq/core';
|
|
10
|
-
import { countSchema, cursorSchema, httpStatusSchema, timeoutMsSchema, windowMsSchema, } from './args/numeric-bounds.js';
|
|
10
|
+
import { countSchema, cursorSchema, httpStatusSchema, timeoutMsSchema, waitForTimeoutMsSchema, resumeAfter, MAX_BLOCKING_WAIT_MS, windowMsSchema, } from './args/numeric-bounds.js';
|
|
11
11
|
import { buildReactionReport } from '@reticlehq/engine/question/reaction.js';
|
|
12
12
|
import { findContradictions } from '@reticlehq/engine/disagreement/contradictions.js';
|
|
13
13
|
import { evaluatePredicate, waitForPredicate, PredicateSchema, } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
@@ -16,7 +16,7 @@ import { WALL_CLOCK } from '../../portal/session/timing/wall-clock.js';
|
|
|
16
16
|
import { parsePredicate } from '@reticlehq/engine/question/predicate/predicate-parse.js';
|
|
17
17
|
import { matchNet, matchConsole, isConsoleEvent, eventMatchesFilters, netEmptyHint, consoleEmptyHint, reconcileNet, projectNetCall, projectConsoleLog, withoutUrlRaw, } from '@reticlehq/engine/window/event-filters.js';
|
|
18
18
|
import { applyEventBudget, costHint, withSizeCost, DEFAULT_OBSERVE_EVENT_LIMIT, DEFAULT_QUERY_LIMIT, } from '../../portal/session/output-budget.js';
|
|
19
|
-
import { annotateStarvedFailure, healthEnvelope, bufferEnvelope, } from '../../portal/session/session-health.js';
|
|
19
|
+
import { annotateStarvedFailure, currentOf, healthEnvelope, bufferEnvelope, } from '../../portal/session/session-health.js';
|
|
20
20
|
import { assertsDerivedIpcStatus, DERIVED_IPC_STATUS_ADVICE, isPresenceOnlyAssertion, PRESENCE_ONLY_ADVICE, } from './assert/assert-grade.js';
|
|
21
21
|
import { assertVerdict } from './assert/assert-verdict.js';
|
|
22
22
|
import { followLostObservation } from './act/act-observation.js';
|
|
@@ -172,6 +172,7 @@ export const OBSERVE_TOOLS = [
|
|
|
172
172
|
currentDocumentId: session.currentDocumentId,
|
|
173
173
|
currentEditEpoch: session.currentEditEpoch,
|
|
174
174
|
appOrigin: session.url,
|
|
175
|
+
background: session.background,
|
|
175
176
|
...(judgingTheAct ? { ...session.lastAct.effect(), actionSince: actCursor } : {}),
|
|
176
177
|
});
|
|
177
178
|
// carry session health — a throttled tab means the observed timeline may be incomplete.
|
|
@@ -193,15 +194,19 @@ export const OBSERVE_TOOLS = [
|
|
|
193
194
|
predicate: PredicateSchema.optional().describe('Predicate to wait for: { signal }, { net }, { element }, { kind: "net", ok: false } (assert on the OUTCOME — the honest field for IPC, which has no status code), { kind: "state", store, path, equals } (assert a registered store\'s value directly — the source of truth no DOM read can reach; equals takes a literal or { $gte | $contains | $length } pattern), { kind: "settled", quietMs } (deterministic network + DOM idle — prefer this over a fixed sleep), or a combination via allOf/anyOf.'),
|
|
194
195
|
// Same concept, the neighbouring tool's name. See alias-args.ts.
|
|
195
196
|
until: PredicateSchema.optional().describe("Alias for `predicate` (act_and_wait's name)."),
|
|
196
|
-
timeout_ms:
|
|
197
|
+
timeout_ms: waitForTimeoutMsSchema
|
|
197
198
|
.optional()
|
|
198
|
-
.describe(
|
|
199
|
+
.describe(`Maximum wait in milliseconds. Default: 4000, up to 600000. One call waits at most ${String(MAX_BLOCKING_WAIT_MS)} ms, because your MCP client aborts a longer request; past that it returns resume_ms, and you call again with timeout_ms: resume_ms and the same since to keep waiting.`),
|
|
199
200
|
since: cursorSchema
|
|
200
201
|
.optional()
|
|
201
202
|
.describe('Cursor from a prior reticle_act — scopes the wait to events after that act.'),
|
|
202
203
|
...sessionIdShape,
|
|
203
204
|
},
|
|
204
205
|
outputSchema: {
|
|
206
|
+
resume_ms: z
|
|
207
|
+
.number()
|
|
208
|
+
.optional()
|
|
209
|
+
.describe('Present when this call stopped at the per-call limit before the predicate was seen. Call again with timeout_ms set to this, the same predicate and the same since, to keep waiting.'),
|
|
205
210
|
pass: z.boolean(),
|
|
206
211
|
evidence: z.unknown().optional(),
|
|
207
212
|
failureReason: z.string().optional(),
|
|
@@ -239,7 +244,9 @@ export const OBSERVE_TOOLS = [
|
|
|
239
244
|
.describe('Where the code behind this verdict lives, as `file:line`. For an element/text assertion it is the matched element itself; for a FAILING signal/net/state assertion — which has no element to point at — it is the control last acted on, where the handler that should have fired lives. OMITTED when neither is known: this tool never borrows an unrelated location.'),
|
|
240
245
|
},
|
|
241
246
|
handler: async (deps, args) => {
|
|
242
|
-
const
|
|
247
|
+
const requestedMs = asNumber(args['timeout_ms']) ?? DEFAULT_ASSERT_TIMEOUT_MS;
|
|
248
|
+
// One call never blocks past what the client allows; the rest comes back as resume_ms.
|
|
249
|
+
const waitBudget = Math.min(requestedMs, MAX_BLOCKING_WAIT_MS);
|
|
243
250
|
// Spend the budget waiting for the APP as well as for the predicate. See resolve-within.
|
|
244
251
|
let session = await resolveSessionWithin(deps.sessions, asString(args['sessionId']), waitBudget, WALL_CLOCK);
|
|
245
252
|
// `until` is act_and_wait's name for this — see alias-args.ts.
|
|
@@ -275,6 +282,7 @@ export const OBSERVE_TOOLS = [
|
|
|
275
282
|
...annotateStarvedFailure(session, verdict),
|
|
276
283
|
...assertionSource(session, predicate, verdict),
|
|
277
284
|
...(followed.followed ? { sessionId: session.id } : {}),
|
|
285
|
+
...((resume) => (resume === undefined ? {} : { resume_ms: resume }))(resumeAfter(requestedMs, waitBudget, verdict)),
|
|
278
286
|
...healthEnvelope(session),
|
|
279
287
|
...bufferEnvelope(session),
|
|
280
288
|
});
|
|
@@ -454,7 +462,7 @@ export const OBSERVE_TOOLS = [
|
|
|
454
462
|
...coverage,
|
|
455
463
|
// The SAME pointer the journal keeps, not a second lookup — one verdict, one file:line.
|
|
456
464
|
...(verdictEffect.source === undefined ? {} : { source: verdictEffect.source }),
|
|
457
|
-
...healthEnvelope(session),
|
|
465
|
+
...healthEnvelope(currentOf(deps.sessions, session)),
|
|
458
466
|
...bufferEnvelope(session),
|
|
459
467
|
});
|
|
460
468
|
},
|
|
@@ -22,11 +22,6 @@
|
|
|
22
22
|
import { ActionType, InputModeReason } from '@reticlehq/core';
|
|
23
23
|
import type { Session } from '../../portal/session/session.js';
|
|
24
24
|
import { type ToolDeps } from './tool-kit.js';
|
|
25
|
-
/**
|
|
26
|
-
* Expand a leading `~` against the caller's home directory. Absolute and relative paths pass
|
|
27
|
-
* through unchanged. `home` is injected so unit tests do not depend on the machine's real home.
|
|
28
|
-
*/
|
|
29
|
-
export declare function expandUserPath(rawPath: string, home: string): string;
|
|
30
25
|
/**
|
|
31
26
|
* If the action is `upload` AND the inner args carry `path`, read the file from disk and rewrite
|
|
32
27
|
* the args to `{ content, name, type, __base64: true }` before the ACT command reaches the browser.
|
|
@@ -99,7 +99,7 @@ const UPLOAD_MAX_BYTES = Math.floor((TRANSPORT_LIMITS.MAX_MESSAGE_BYTES / (4 / 3
|
|
|
99
99
|
* Expand a leading `~` against the caller's home directory. Absolute and relative paths pass
|
|
100
100
|
* through unchanged. `home` is injected so unit tests do not depend on the machine's real home.
|
|
101
101
|
*/
|
|
102
|
-
|
|
102
|
+
function expandUserPath(rawPath, home) {
|
|
103
103
|
if ('~' === rawPath)
|
|
104
104
|
return home;
|
|
105
105
|
if (rawPath.startsWith('~/') || rawPath.startsWith('~\\'))
|
|
@@ -222,6 +222,11 @@ export declare const EnvelopeKey: {
|
|
|
222
222
|
readonly VERIFY_NEXT: "verify_next";
|
|
223
223
|
/** The contextual "tell us what just went wrong" invitation. Same story, same release. */
|
|
224
224
|
readonly FEEDBACK_INVITE: "feedback_invite";
|
|
225
|
+
/**
|
|
226
|
+
* The same moment, for the HUMAN: a call with the founder, or an email through `identify`. The
|
|
227
|
+
* agent is told to offer it and never to book or submit anything itself.
|
|
228
|
+
*/
|
|
229
|
+
readonly TALK_TO_US: "talk_to_us";
|
|
225
230
|
/** SDK/daemon version skew — often the one fact that explains everything else in the session. */
|
|
226
231
|
readonly VERSION_SKEW: "version_skew";
|
|
227
232
|
/** A feedback report that was accepted and then failed to send. Only the reporter can act on it. */
|
|
@@ -28,7 +28,7 @@ export const intentArg = z
|
|
|
28
28
|
'BAD: "P1 step 5", "drive 2 of 2", "same as before the fix", "the board renders cleanly" — these name a session, ' +
|
|
29
29
|
'a step number or nothing checkable, and the person reading them later has none of that context. ' +
|
|
30
30
|
'This is shared memory: it is pooled per project and later agents read it back to avoid re-deriving what you just established. ' +
|
|
31
|
-
'Recorded in .reticle/intent
|
|
31
|
+
'Recorded in .reticle/intent/, the same ledger reticle_intent writes, and marked proved by this verdict if it passes.');
|
|
32
32
|
export const sessionIdShape = {
|
|
33
33
|
sessionId: z
|
|
34
34
|
.string()
|
|
@@ -75,6 +75,11 @@ export const EnvelopeKey = {
|
|
|
75
75
|
VERIFY_NEXT: 'verify_next',
|
|
76
76
|
/** The contextual "tell us what just went wrong" invitation. Same story, same release. */
|
|
77
77
|
FEEDBACK_INVITE: 'feedback_invite',
|
|
78
|
+
/**
|
|
79
|
+
* The same moment, for the HUMAN: a call with the founder, or an email through `identify`. The
|
|
80
|
+
* agent is told to offer it and never to book or submit anything itself.
|
|
81
|
+
*/
|
|
82
|
+
TALK_TO_US: 'talk_to_us',
|
|
78
83
|
/** SDK/daemon version skew — often the one fact that explains everything else in the session. */
|
|
79
84
|
VERSION_SKEW: 'version_skew',
|
|
80
85
|
/** A feedback report that was accepted and then failed to send. Only the reporter can act on it. */
|
|
@@ -395,11 +395,23 @@ export function describeToolSurface(active, requested) {
|
|
|
395
395
|
export function defaultAdvertisedNames() {
|
|
396
396
|
const surface = resolveToolSurface();
|
|
397
397
|
const names = [...filterToolNames(surface)];
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
398
|
+
/*
|
|
399
|
+
* Both meta tools, on every surface, because that is what the MCP actually advertises.
|
|
400
|
+
*
|
|
401
|
+
* This used to withhold `reticle_run` from the merged surface, reasoning that "naming a hatch
|
|
402
|
+
* that is not there is the same defect one level in". The reasoning is sound and the premise was
|
|
403
|
+
* false: `buildDynamicTools` returns `[reticle_tools, reticle_run]` unconditionally and
|
|
404
|
+
* `advertisedTools` appends both to every surface, so the hatch has always been there. Measured
|
|
405
|
+
* against a live daemon on the default surface: `reticle_run` is advertised, and
|
|
406
|
+
* `reticle_run { tool: "reticle_lease" }` reaches `reticle_lease`.
|
|
407
|
+
*
|
|
408
|
+
* Two functions answering one question, and this was the one the MCP never called. It is read by
|
|
409
|
+
* `server-instructions`, which gates "Everything else is one hop: reticle_tools lists it,
|
|
410
|
+
* reticle_run calls it" on this list - so agents were told the hatch was absent from a surface
|
|
411
|
+
* carrying it, and six recovery messages were later rewritten to match the lie.
|
|
412
|
+
* `advertised-names-are-real.test.ts` now pins this to `advertisedTools` itself.
|
|
413
|
+
*/
|
|
414
|
+
return [...names, ReticleTool.TOOLS, ReticleTool.RUN];
|
|
403
415
|
}
|
|
404
416
|
/** The advertised NAME set for a surface, without needing the tool table. */
|
|
405
417
|
function filterToolNames(surface) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { NoSessionAction, QueryBy, ReticleCommand, SnapshotMode } from '@reticlehq/core';
|
|
2
|
+
import { DiscoveryInvite, AttrNamesSchema, EventType, NoSessionAction, QueryBy, ReticleCommand, SnapshotMode, } from '@reticlehq/core';
|
|
3
3
|
import { ReticleTool } from '@reticlehq/core';
|
|
4
4
|
import { withSizeCost } from '../../portal/session/output-budget.js';
|
|
5
5
|
import { applySnapshotDelta, SnapshotCache } from './read/snapshot-delta.js';
|
|
@@ -109,7 +109,7 @@ export const RAW_TOOLS = [
|
|
|
109
109
|
lastOutage: z.object({ startedMs: z.number(), durationMs: z.number() }).optional(),
|
|
110
110
|
})
|
|
111
111
|
.optional()
|
|
112
|
-
.describe('
|
|
112
|
+
.describe('Continuity of the SDK-to-daemon LINK for this tab, present for any tab the daemon has a record of: how long this attachment has lasted, how many times it dropped and came back, and when the last gap started and how long it lasted. `outages > 0` means a verdict over this window spans a gap where nothing was observed. These are link counters, NOT app health: they say nothing about whether the page origin is still serving, so a tab whose dev server has died reports `outages: 0` with a growing `connectedSinceMs` and is not evidence the app is up.'),
|
|
113
113
|
}))
|
|
114
114
|
.describe('Connected browser sessions with health state. `projectId` groups sessions by app (stable across port changes); `leased` marks a pool-managed headless context vs a human tab.'),
|
|
115
115
|
why: z
|
|
@@ -125,6 +125,10 @@ export const RAW_TOOLS = [
|
|
|
125
125
|
})
|
|
126
126
|
.optional()
|
|
127
127
|
.describe("Present ONLY when `sessions` is empty: the same answer as `why`, executable. `command` is the LITERAL command to run, sourced from this project's own package.json scripts and lockfile — it is absent, never guessed, when the project declares no dev script. `action` is one of daemon_split | start_dev_server | run_init | open_app | reopen_app. `daemon_split` outranks the rest and means the app IS running and instrumented, on a DIFFERENT daemon than the one you are attached to — do not start or re-init anything, read `reason`."),
|
|
128
|
+
talk_to_us: z
|
|
129
|
+
.string()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe('Present ONLY when `sessions` is empty: a call with the founder for the person you are working with. Offer it; never act on it.'),
|
|
128
132
|
lastKnown: z
|
|
129
133
|
.object({
|
|
130
134
|
sessionId: z.string(),
|
|
@@ -158,6 +162,8 @@ export const RAW_TOOLS = [
|
|
|
158
162
|
...(why === undefined ? {} : { why }),
|
|
159
163
|
...(next === undefined ? {} : { next_action: next }),
|
|
160
164
|
...(undefined === lastKnown ? {} : { lastKnown }),
|
|
165
|
+
// Setup that never connects is where most people give up; it is the talk worth having.
|
|
166
|
+
talk_to_us: DiscoveryInvite.AGENT,
|
|
161
167
|
};
|
|
162
168
|
}
|
|
163
169
|
return { sessions };
|
|
@@ -261,7 +267,7 @@ export const RAW_TOOLS = [
|
|
|
261
267
|
scope: asString(args['scope']) ?? '',
|
|
262
268
|
mode,
|
|
263
269
|
diff: true === args['diff'],
|
|
264
|
-
}, deps.snapshots ?? SNAPSHOT_CACHE), mode), mode), mode)));
|
|
270
|
+
}, deps.snapshots ?? SNAPSHOT_CACHE), mode), mode), mode, resolved)));
|
|
265
271
|
},
|
|
266
272
|
},
|
|
267
273
|
{
|
|
@@ -290,10 +296,7 @@ export const RAW_TOOLS = [
|
|
|
290
296
|
.boolean()
|
|
291
297
|
.optional()
|
|
292
298
|
.describe('Return the `scope` element ITSELF instead of searching inside it. Use when the target is a plain layout container with no role, name, testid or text of its own — which is routinely the element that carries the handler, and is otherwise unreachable because every query excludes its own scope root. Requires `scope`.'),
|
|
293
|
-
attrs:
|
|
294
|
-
.array(z.string())
|
|
295
|
-
.optional()
|
|
296
|
-
.describe("Attribute names to return per match, e.g. ['href'] to inventory links or ['src'] for images. Absent attributes are omitted; credential-bearing names are redacted."),
|
|
299
|
+
attrs: AttrNamesSchema.optional().describe("Attribute NAMES to return per match, e.g. ['href'] for links, ['src'] for images. Projects, never filters: 'name=value' is refused. Absent omitted; credentials redacted."),
|
|
297
300
|
limit: countSchema
|
|
298
301
|
.optional()
|
|
299
302
|
.describe('Cap the returned descriptors to the first N (cuts tokens on broad queries). If more matched, the result carries total + truncated:true so the trim is never silent — narrow with name/scope.'),
|
|
@@ -371,6 +374,12 @@ export const RAW_TOOLS = [
|
|
|
371
374
|
.optional()
|
|
372
375
|
.describe('Structural clusters on the page — what IS here, to diagnose the miss.'),
|
|
373
376
|
presentTestids: z.array(z.string()).optional(),
|
|
377
|
+
// Declared for the same reason presentRegions had to be: undeclared here, it is stripped
|
|
378
|
+
// from structuredContent without a word — and this field exists precisely to say a word.
|
|
379
|
+
presentTestidsTotal: z
|
|
380
|
+
.number()
|
|
381
|
+
.optional()
|
|
382
|
+
.describe('Present only when presentTestids was cut at its cap: how many there were. The list is document order, so a region low on the page is what got dropped — absence from the list proves nothing.'),
|
|
374
383
|
knownEmptyState: z.boolean(),
|
|
375
384
|
splitText: z
|
|
376
385
|
.object({
|
|
@@ -614,24 +623,31 @@ function noteEmptyLeanTree(result, mode) {
|
|
|
614
623
|
* wrapper or two is ordinary, so this is deliberately not `=== 0`.
|
|
615
624
|
*/
|
|
616
625
|
const UNMOUNTED_ROOT_MAX_ELEMENTS = 3;
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
626
|
+
function crashEvidence(session) {
|
|
627
|
+
const since = asRecord(session)['eventsSince'];
|
|
628
|
+
if ('function' !== typeof since)
|
|
629
|
+
return undefined;
|
|
630
|
+
let events;
|
|
631
|
+
try {
|
|
632
|
+
events = since.call(session, 0);
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
if (!Array.isArray(events))
|
|
638
|
+
return undefined;
|
|
639
|
+
const errors = events.filter((e) => {
|
|
640
|
+
const type = asString(asRecord(e)['type']);
|
|
641
|
+
return EventType.CONSOLE_ERROR === type || EventType.ERROR_UNCAUGHT === type;
|
|
642
|
+
});
|
|
643
|
+
if (0 === errors.length)
|
|
644
|
+
return undefined;
|
|
645
|
+
const first = errors
|
|
646
|
+
.map((e) => asString(asRecord(asRecord(e)['data'])['message']))
|
|
647
|
+
.find((m) => undefined !== m && '' !== m);
|
|
648
|
+
return { count: errors.length, first: first ?? '' };
|
|
649
|
+
}
|
|
650
|
+
function noteUnmountedRoot(result, mode, session) {
|
|
635
651
|
if (SnapshotMode.STATUS === mode)
|
|
636
652
|
return result;
|
|
637
653
|
const row = asRecord(result);
|
|
@@ -642,12 +658,31 @@ function noteUnmountedRoot(result, mode) {
|
|
|
642
658
|
const elements = asNumber(row['domElements']);
|
|
643
659
|
if (elements === undefined || elements > UNMOUNTED_ROOT_MAX_ELEMENTS)
|
|
644
660
|
return result;
|
|
661
|
+
const mount = `the tree is empty because there is almost nothing in the DOM: ${String(elements)} ` +
|
|
662
|
+
`element(s) under this scope. That is a mount container with nothing rendered into it, not a ` +
|
|
663
|
+
`page whose contents were skipped — so the app is UNMOUNTED rather than slow, and waiting ` +
|
|
664
|
+
`will not change it. `;
|
|
665
|
+
// An uncaught error beside an empty mount container is the difference between "has not started"
|
|
666
|
+
// and "started and died", which are the two readings of this count and want opposite next moves.
|
|
667
|
+
// Nothing here is framework-specific, and deliberately so: the condition is a known mount
|
|
668
|
+
// container that holds nothing with an uncaught error beside it, which is as true of a Vue or
|
|
669
|
+
// Svelte root as a React one.
|
|
670
|
+
const crash = crashEvidence(session);
|
|
671
|
+
if (crash !== undefined) {
|
|
672
|
+
return {
|
|
673
|
+
...row,
|
|
674
|
+
note: mount +
|
|
675
|
+
`The app did not merely fail to start — it CRASHED: ${String(crash.count)} uncaught ` +
|
|
676
|
+
`error(s) were logged in this session` +
|
|
677
|
+
('' === crash.first ? '' : `, the first: "${crash.first}"`) +
|
|
678
|
+
`. Read reticle_console for the rest, and check anything that runs inside a framework ` +
|
|
679
|
+
`commit phase. Loading the app again will reproduce it rather than fix it.`,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
645
682
|
return {
|
|
646
683
|
...row,
|
|
647
|
-
note:
|
|
648
|
-
`
|
|
649
|
-
`page whose contents were skipped — so the app is UNMOUNTED rather than slow, and waiting ` +
|
|
650
|
-
`will not change it. An app that was rendering and then stopped has usually thrown: read ` +
|
|
684
|
+
note: mount +
|
|
685
|
+
`An app that was rendering and then stopped has usually thrown: read ` +
|
|
651
686
|
`reticle_console for an uncaught error, and check anything that runs inside a framework ` +
|
|
652
687
|
`commit phase. If the app has genuinely not started yet, load it and snapshot again.`,
|
|
653
688
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Feedback } from '@reticlehq/core/telemetry';
|
|
2
|
+
/** Reports live beside the rest of the workspace, not in a temp dir somebody has to be told about. */
|
|
2
3
|
interface SavedFeedback {
|
|
3
4
|
path: string;
|
|
4
5
|
/** A ready-to-run command that turns the file into an issue. */
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { ReticleDir } from '@reticlehq/core';
|
|
16
|
-
/** Reports live beside the rest of the workspace, not in a temp dir somebody has to be told about. */
|
|
17
|
-
const FEEDBACK_SUBDIR = 'feedback';
|
|
18
16
|
/** Filesystem-safe, sortable, and unique enough for one report per second per kind. */
|
|
19
17
|
const stamp = (now) => new Date(now).toISOString().replace(/[:.]/g, '-').replace('Z', '');
|
|
20
18
|
const asMarkdown = (input, text, context) => {
|
|
@@ -39,7 +37,7 @@ const asMarkdown = (input, text, context) => {
|
|
|
39
37
|
*/
|
|
40
38
|
export function saveFeedbackLocally(cwd, input, text, context, now = Date.now()) {
|
|
41
39
|
try {
|
|
42
|
-
const dir = join(cwd, ReticleDir.ROOT, FEEDBACK_SUBDIR);
|
|
40
|
+
const dir = join(cwd, ReticleDir.ROOT, ReticleDir.FEEDBACK_SUBDIR);
|
|
43
41
|
mkdirSync(dir, { recursive: true });
|
|
44
42
|
const path = join(dir, `${stamp(now)}-${input.kind ?? 'note'}.md`);
|
|
45
43
|
writeFileSync(path, asMarkdown(input, text, context), 'utf8');
|
|
@@ -27,7 +27,7 @@ import { type SessionFacts } from './feedback-context.js';
|
|
|
27
27
|
export declare const FEEDBACK_ENV = "RETICLE_FEEDBACK";
|
|
28
28
|
/** What replaces a redacted match. Visible on purpose: a reader must see that something was removed. */
|
|
29
29
|
export declare const REDACTED = "[redacted]";
|
|
30
|
-
|
|
30
|
+
interface Redaction {
|
|
31
31
|
text: string;
|
|
32
32
|
/** Which rules fired, deduped. Surfaced back to the author so a redaction is never silent. */
|
|
33
33
|
removed: string[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { HudPanel, HudView, type HudUseData } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* How people use the in-page HUD, rolled into the session summary.
|
|
4
|
+
*
|
|
5
|
+
* Windowed like the tool histograms beside it: a periodic flush reports what happened since the
|
|
6
|
+
* last one, so a dashboard sums across a session's events. Names only — every key is a control id
|
|
7
|
+
* the wire schema already checked against core's closed list.
|
|
8
|
+
*/
|
|
9
|
+
/** How many steps of the journey one window keeps. A journey longer than this is cut, and says so. */
|
|
10
|
+
export declare const HUD_JOURNEY_MAX = 60;
|
|
11
|
+
export interface HudSummary {
|
|
12
|
+
/** Presses per control; a toggle counts under `id:on` / `id:off` so a switch reads both ways. */
|
|
13
|
+
hudControls?: Record<string, number>;
|
|
14
|
+
/** Milliseconds the HUD spent in each view during the window. */
|
|
15
|
+
hudViewMs?: Partial<Record<HudView, number>>;
|
|
16
|
+
/** Milliseconds each panel was showing during the window. */
|
|
17
|
+
hudPanelMs?: Partial<Record<HudPanel, number>>;
|
|
18
|
+
/** The window's presses and view changes, in order, cut at HUD_JOURNEY_MAX. */
|
|
19
|
+
hudJourney?: string[];
|
|
20
|
+
/** Present, and true, only when the journey was cut. */
|
|
21
|
+
hudJourneyCut?: true;
|
|
22
|
+
}
|
|
23
|
+
export declare class HudMetrics {
|
|
24
|
+
#private;
|
|
25
|
+
constructor(now: () => number);
|
|
26
|
+
record(use: HudUseData): void;
|
|
27
|
+
get empty(): boolean;
|
|
28
|
+
/** The window so far. Open view and panel are counted up to now, and keep running after. */
|
|
29
|
+
summarize(): HudSummary;
|
|
30
|
+
/** Start the next window. The current view and panel carry over, timed from now. */
|
|
31
|
+
reset(): void;
|
|
32
|
+
}
|