@reticlehq/server 3.8.1 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -104
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/bin/launch.js +13 -0
- package/bin/package.json +3 -0
- package/bin/reticle.js +66 -8
- package/dist/chromium-launch-options.d.ts +25 -0
- package/dist/chromium-launch-options.js +21 -1
- package/dist/command/cli/adhoc-verdict.js +34 -7
- package/dist/command/cli/answers/gate-exit.d.ts +6 -2
- package/dist/command/cli/answers/gate-exit.js +12 -2
- package/dist/command/cli/ci-hint.d.ts +44 -0
- package/dist/command/cli/ci-hint.js +72 -0
- package/dist/command/cli/cli-doctor.d.ts +6 -1
- package/dist/command/cli/cli-doctor.js +45 -4
- package/dist/command/cli/cli-flow-commands.d.ts +7 -1
- package/dist/command/cli/cli-flow-commands.js +39 -17
- package/dist/command/cli/cli-kill.d.ts +21 -0
- package/dist/command/cli/cli-kill.js +46 -3
- package/dist/command/cli/cli-parse.d.ts +5 -2
- package/dist/command/cli/cli-parse.js +10 -5
- package/dist/command/cli/cli-usage.js +3 -2
- package/dist/command/cli/cli-verify.d.ts +6 -1
- package/dist/command/cli/cli-verify.js +32 -13
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
- package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
- package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
- package/dist/command/cli/doctor/doctor-fix.js +89 -0
- package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/doctor-repairs.d.ts +64 -0
- package/dist/command/cli/doctor-repairs.js +214 -0
- package/dist/command/cli/drive/drive-command.js +3 -7
- package/dist/command/cli/first-use-checks.d.ts +41 -0
- package/dist/command/cli/first-use-checks.js +127 -0
- package/dist/command/cli/first-verdict-node.d.ts +3 -0
- package/dist/command/cli/first-verdict-node.js +189 -0
- package/dist/command/cli/first-verdict.d.ts +126 -0
- package/dist/command/cli/first-verdict.js +267 -0
- package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
- package/dist/command/cli/launch/sdk-version-match.js +3 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
- package/dist/command/cli/mcp-command.js +13 -4
- package/dist/command/cli/ports/port-holder.d.ts +2 -0
- package/dist/command/cli/ports/port-holder.js +11 -3
- package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
- package/dist/command/cli/setup-mcp-cli.js +98 -23
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +50 -17
- package/dist/command/cli/status-fields.js +17 -3
- package/dist/command/cli/try-command.d.ts +7 -0
- package/dist/command/cli/try-command.js +11 -3
- package/dist/command/cli.js +70 -33
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
- package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
- package/dist/command/daemon/daemon-argv.d.ts +9 -0
- package/dist/command/daemon/daemon-argv.js +11 -0
- package/dist/command/daemon/daemon.d.ts +10 -3
- package/dist/command/daemon/daemon.js +17 -6
- package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
- package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
- package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
- package/dist/command/daemon/lifetime/death-cause.js +130 -0
- package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
- package/dist/command/daemon/lifetime/heartbeat.js +4 -13
- package/dist/command/daemon/port-state.d.ts +14 -0
- package/dist/command/daemon/port-state.js +31 -0
- package/dist/command/setup/agent-configs.d.ts +2 -0
- package/dist/command/setup/agent-configs.js +42 -15
- package/dist/command/setup/agent-next-steps.d.ts +77 -0
- package/dist/command/setup/agent-next-steps.js +284 -0
- package/dist/command/setup/agent-writer.js +11 -6
- package/dist/command/setup/init/init-host.js +2 -0
- package/dist/command/setup/init/init-runtime.d.ts +6 -1
- package/dist/command/setup/init/init-runtime.js +57 -21
- package/dist/command/setup/mcp-launcher.d.ts +3 -0
- package/dist/command/setup/mcp-launcher.js +11 -0
- package/dist/command/setup/setup-command.js +30 -12
- package/dist/command/setup/setup-mcp.d.ts +8 -0
- package/dist/command/setup/setup-mcp.js +47 -20
- package/dist/command/setup/start-dev-server.d.ts +8 -0
- package/dist/command/setup/start-dev-server.js +43 -0
- package/dist/command/version/identity/server-version.d.ts +2 -0
- package/dist/command/version/identity/server-version.js +2 -0
- package/dist/command/version/version-skew.d.ts +7 -0
- package/dist/command/version/version-skew.js +13 -2
- package/dist/features/license/license.d.ts +11 -71
- package/dist/features/license/license.js +43 -136
- package/dist/features/visual/desktop-capture.d.ts +33 -0
- package/dist/features/visual/desktop-capture.js +94 -0
- package/dist/features/visual/step-shots.js +139 -35
- package/dist/features/visual/visual-tools.d.ts +0 -18
- package/dist/features/visual/visual-tools.js +2 -81
- package/dist/import-graph.js +5 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +14 -5
- package/dist/judgement/runs/run-ledger.d.ts +7 -5
- package/dist/judgement/runs/run-ledger.js +98 -69
- package/dist/judgement/runs/verification-sync.d.ts +6 -3
- package/dist/judgement/runs/verification-sync.js +21 -10
- package/dist/language/flows/flow-store-for-session.js +7 -5
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
- package/dist/language/flows/recording/tape/recordings.js +41 -44
- package/dist/language/flows/replay.d.ts +17 -7
- package/dist/language/flows/replay.js +95 -44
- package/dist/launch-chromium.d.ts +1 -0
- package/dist/launch-chromium.js +49 -12
- package/dist/memory/cloud/sync-screenshots.js +6 -1
- package/dist/portal/bridge/bridge.d.ts +13 -4
- package/dist/portal/bridge/bridge.js +10 -9
- package/dist/portal/input/drive-error.d.ts +6 -0
- package/dist/portal/input/drive-error.js +9 -0
- package/dist/portal/input/network-mock.js +3 -2
- package/dist/portal/input/real-input-press.d.ts +34 -0
- package/dist/portal/input/real-input-press.js +150 -0
- package/dist/portal/input/real-input.d.ts +21 -23
- package/dist/portal/input/real-input.js +153 -194
- package/dist/portal/input/viewport-tools.js +6 -3
- package/dist/portal/pool/browser-pool.d.ts +5 -1
- package/dist/portal/pool/browser-pool.js +32 -38
- package/dist/portal/pool/context-permissions.d.ts +20 -17
- package/dist/portal/pool/context-permissions.js +63 -1
- package/dist/portal/pool/csp-widen.d.ts +62 -0
- package/dist/portal/pool/csp-widen.js +132 -0
- package/dist/portal/pool/lease-page-error.d.ts +29 -0
- package/dist/portal/pool/lease-page-error.js +18 -0
- package/dist/portal/pool/page-error-journal.d.ts +57 -0
- package/dist/portal/pool/page-error-journal.js +181 -0
- package/dist/portal/pool/playwright-launcher.js +21 -8
- package/dist/portal/pool/pool-contract.d.ts +22 -0
- package/dist/portal/pool/pool-lease.d.ts +35 -2
- package/dist/portal/pool/pool-lease.js +23 -0
- package/dist/portal/pool/zero-install.js +69 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
- package/dist/portal/session/dev-server/dev-command.js +56 -8
- package/dist/portal/session/no-session-diagnosis.js +10 -3
- package/dist/portal/session/no-session-next-action.d.ts +12 -0
- package/dist/portal/session/no-session-next-action.js +44 -4
- package/dist/portal/session/no-session-watch.js +3 -1
- package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
- package/dist/portal/session/page-commands/undeclared-command.js +16 -7
- package/dist/portal/session/recorded-gaps.d.ts +1 -1
- package/dist/portal/session/recorded-gaps.js +5 -2
- package/dist/portal/session/session-manager.js +40 -21
- package/dist/portal/session/session.js +4 -3
- package/dist/portal/session/zero-install-first-call.d.ts +20 -1
- package/dist/portal/session/zero-install-first-call.js +34 -2
- package/dist/start-wiring.d.ts +29 -2
- package/dist/start-wiring.js +73 -9
- package/dist/surface/mcp/mcp-proxy.js +10 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
- package/dist/surface/mcp/peer/agents-connected.js +58 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
- package/dist/surface/tools/act/act-preflight.js +37 -17
- package/dist/surface/tools/act/act-target.js +3 -1
- package/dist/surface/tools/act/after-match.d.ts +7 -0
- package/dist/surface/tools/act/after-match.js +33 -0
- package/dist/surface/tools/act/resolve-target.d.ts +2 -0
- package/dist/surface/tools/act/resolve-target.js +35 -8
- package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
- package/dist/surface/tools/act/sequence-grade.js +8 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -0
- package/dist/surface/tools/act-tools.js +12 -12
- package/dist/surface/tools/assert/assert-verdict.js +28 -1
- package/dist/surface/tools/browser-tools.js +75 -7
- package/dist/surface/tools/dynamic-tools.js +2 -1
- package/dist/surface/tools/empty-session-list.d.ts +2 -0
- package/dist/surface/tools/empty-session-list.js +4 -2
- package/dist/surface/tools/error-recovery.js +43 -22
- package/dist/surface/tools/invoke-tool.js +27 -20
- package/dist/surface/tools/lease-hint.d.ts +7 -0
- package/dist/surface/tools/lease-hint.js +10 -3
- package/dist/surface/tools/lease-readiness.d.ts +21 -0
- package/dist/surface/tools/lease-readiness.js +47 -0
- package/dist/surface/tools/lease-seed.d.ts +2 -0
- package/dist/surface/tools/lease-seed.js +2 -1
- package/dist/surface/tools/lease-tools.js +43 -15
- package/dist/surface/tools/real-input-attempt.js +70 -31
- package/dist/surface/tools/repeat-hint.d.ts +40 -0
- package/dist/surface/tools/repeat-hint.js +171 -0
- package/dist/surface/tools/run-report.d.ts +2 -2
- package/dist/surface/tools/run-report.js +27 -15
- package/dist/surface/tools/tool-kit.d.ts +12 -0
- package/dist/surface/tools/tool-kit.js +7 -0
- package/dist/surface/tools/tools.js +26 -13
- package/dist/telemetry/argument-shape.js +16 -7
- package/dist/telemetry/cli-telemetry.d.ts +10 -1
- package/dist/telemetry/cli-telemetry.js +15 -0
- package/dist/telemetry/init-telemetry.d.ts +2 -1
- package/dist/telemetry/init-telemetry.js +47 -2
- package/dist/telemetry/install-trace.js +23 -4
- package/dist/telemetry/onboarding-funnel.d.ts +19 -2
- package/dist/telemetry/onboarding-funnel.js +25 -3
- package/dist/telemetry/proxy-telemetry.d.ts +2 -1
- package/dist/telemetry/proxy-telemetry.js +2 -2
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +67 -27
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +60 -28
- package/dist/telemetry/tool-refused.d.ts +2 -2
- package/dist/telemetry/tool-refused.js +9 -2
- package/docs/architecture.md +3 -6
- package/docs/cli/doctor.mdx +41 -2
- package/docs/cli/help.mdx +4 -2
- package/docs/cli/license.mdx +4 -5
- package/docs/cli/setup.mdx +13 -2
- package/docs/cli/status.mdx +4 -0
- package/docs/cli.mdx +1 -1
- package/docs/deploy-checks.md +25 -0
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +15 -20
- package/docs/environment.mdx +1 -0
- package/docs/first-drive.md +1 -1
- package/docs/install-manual.mdx +16 -1
- package/docs/packages/engine.mdx +1 -1
- package/docs/packages/react.mdx +14 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +1 -0
- package/docs/packages.mdx +1 -1
- package/docs/platform-integration.md +1 -1
- package/docs/skill.md +1 -1
- package/docs/telemetry.md +4 -2
- package/package.json +14 -10
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
package/dist/start-wiring.d.ts
CHANGED
|
@@ -3,14 +3,39 @@ import type { StartOptions } from './start-options.js';
|
|
|
3
3
|
import type { Bridge } from './portal/bridge/bridge.js';
|
|
4
4
|
import type { RealInputProvider } from './portal/input/real-input.js';
|
|
5
5
|
import { BrowserPool } from './portal/pool/browser-pool.js';
|
|
6
|
+
import { type PageErrorJournal } from './portal/pool/page-error-journal.js';
|
|
7
|
+
import type { PooledPageError } from './portal/pool/pool-contract.js';
|
|
6
8
|
import { type InjectedConnect } from './portal/pool/zero-install.js';
|
|
9
|
+
import type { NonceMinter } from './portal/production/production-activation.js';
|
|
7
10
|
/**
|
|
8
11
|
* Build the shared browser pool (one headless Chromium, N capped isolated leased contexts). Lazy —
|
|
9
12
|
* no Chromium launches until the first lease — so creating it is free even when never used.
|
|
10
13
|
*/
|
|
11
14
|
/** Where the zero-install reader connects: this daemon's own bridge, with its pairing token. */
|
|
12
15
|
export declare function readerConnect(port: number, token: string | undefined): InjectedConnect;
|
|
13
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The zero-install script for pages that connect to `reader`, built on first use and kept: the
|
|
18
|
+
* page that needs it is one whose app never connected. Undefined when this install has no reader.
|
|
19
|
+
*
|
|
20
|
+
* A page that is not on loopback gets a nonce minted for its origin in place of the pairing token.
|
|
21
|
+
* The bridge admits a WebSocket from an origin only while a nonce names it, so the reader on a
|
|
22
|
+
* deployed page was refused with a 403 (#2016). The nonce also keeps the machine's pairing token
|
|
23
|
+
* out of a page that somebody else serves.
|
|
24
|
+
*
|
|
25
|
+
* ponytail: the nonce lives for one drive (SESSION_NONCE_TTL_MS). A lease reloaded after that
|
|
26
|
+
* dials with a dead nonce; mint per navigation if that is ever seen.
|
|
27
|
+
*/
|
|
28
|
+
export declare function zeroInstallLoader(reader: InjectedConnect, nonces?: Pick<NonceMinter, 'mint'>): (url?: string) => string | undefined;
|
|
29
|
+
export declare function createBrowserPool(headless: boolean, reader: InjectedConnect, onPageError?: (leaseId: string, error: PooledPageError) => void, onLeaseClosed?: (leaseId: string) => void, nonces?: Pick<NonceMinter, 'mint'>): BrowserPool;
|
|
30
|
+
/**
|
|
31
|
+
* Journal the uncaught page errors the pool hears into the session that connects for that lease.
|
|
32
|
+
*
|
|
33
|
+
* The pool sees every throw its pages make from before the first navigation; the SDK only exists
|
|
34
|
+
* afterwards. Without this wiring an import-time crash — a blank page with no error text anywhere
|
|
35
|
+
* (#1966) — was invisible to the agent driving it. Returns the journal so the pool's sink can be
|
|
36
|
+
* handed over; the bridge hook is registered here.
|
|
37
|
+
*/
|
|
38
|
+
export declare function attachPageErrorJournal(bridge: Bridge): PageErrorJournal;
|
|
14
39
|
/**
|
|
15
40
|
* Route CDP-authoritative network detail onto the driven session's journal.
|
|
16
41
|
*
|
|
@@ -26,7 +51,9 @@ export declare function makeNetworkDetailRouter(bridge: Bridge, driveUrl: string
|
|
|
26
51
|
* so a failed launch never leaks a WS port. A past divergence between the two paths let daemon mode
|
|
27
52
|
* run with a different setup — this removes that risk.
|
|
28
53
|
*/
|
|
29
|
-
export declare function resolveRealInput(options: StartOptions, onNavigateError: () => Promise<void>, onNetworkDetail?: (detail: NetworkDetail) => void
|
|
54
|
+
export declare function resolveRealInput(options: StartOptions, onNavigateError: () => Promise<void>, onNetworkDetail?: (detail: NetworkDetail) => void,
|
|
55
|
+
/** Where a driven page with no SDK connects the zero-install reader. Absent: it gets none. */
|
|
56
|
+
reader?: InjectedConnect): Promise<{
|
|
30
57
|
realInput?: RealInputProvider;
|
|
31
58
|
owned?: {
|
|
32
59
|
dispose: () => Promise<void>;
|
package/dist/start-wiring.js
CHANGED
|
@@ -23,7 +23,10 @@ import { SERVER_VERSION } from './command/version/identity/server-version.js';
|
|
|
23
23
|
import { CdpRealInputProvider, LaunchedRealInputProvider } from './portal/input/real-input.js';
|
|
24
24
|
import { BrowserPool } from './portal/pool/browser-pool.js';
|
|
25
25
|
import { playwrightLauncher, resolveMaxContexts } from './portal/pool/playwright-launcher.js';
|
|
26
|
+
import { makePageErrorJournal } from './portal/pool/page-error-journal.js';
|
|
27
|
+
import { resolveLeasedSessionId } from './surface/tools/lease-tools.js';
|
|
26
28
|
import { readReaderBundle, zeroInstallScript as zeroInstallScriptFor, } from './portal/pool/zero-install.js';
|
|
29
|
+
import { offLoopbackOrigin } from './portal/pool/context-permissions.js';
|
|
27
30
|
/**
|
|
28
31
|
* Build the shared browser pool (one headless Chromium, N capped isolated leased contexts). Lazy —
|
|
29
32
|
* no Chromium launches until the first lease — so creating it is free even when never used.
|
|
@@ -32,26 +35,84 @@ import { readReaderBundle, zeroInstallScript as zeroInstallScriptFor, } from './
|
|
|
32
35
|
export function readerConnect(port, token) {
|
|
33
36
|
return { url: bridgeWsUrl(port), ...(token === undefined ? {} : { token }) };
|
|
34
37
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The zero-install script for pages that connect to `reader`, built on first use and kept: the
|
|
40
|
+
* page that needs it is one whose app never connected. Undefined when this install has no reader.
|
|
41
|
+
*
|
|
42
|
+
* A page that is not on loopback gets a nonce minted for its origin in place of the pairing token.
|
|
43
|
+
* The bridge admits a WebSocket from an origin only while a nonce names it, so the reader on a
|
|
44
|
+
* deployed page was refused with a 403 (#2016). The nonce also keeps the machine's pairing token
|
|
45
|
+
* out of a page that somebody else serves.
|
|
46
|
+
*
|
|
47
|
+
* ponytail: the nonce lives for one drive (SESSION_NONCE_TTL_MS). A lease reloaded after that
|
|
48
|
+
* dials with a dead nonce; mint per navigation if that is ever seen.
|
|
49
|
+
*/
|
|
50
|
+
export function zeroInstallLoader(reader, nonces) {
|
|
41
51
|
let script;
|
|
42
|
-
|
|
52
|
+
return (url) => {
|
|
53
|
+
const origin = offLoopbackOrigin(url);
|
|
54
|
+
if (origin !== undefined && nonces !== undefined) {
|
|
55
|
+
const bundle = readReaderBundle();
|
|
56
|
+
if (bundle === undefined) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return zeroInstallScriptFor(bundle, {
|
|
60
|
+
url: reader.url,
|
|
61
|
+
token: nonces.mint({ origin }).nonce,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
43
64
|
if (script === undefined) {
|
|
44
65
|
const bundle = readReaderBundle();
|
|
45
66
|
script = bundle === undefined ? null : zeroInstallScriptFor(bundle, reader);
|
|
46
67
|
}
|
|
47
68
|
return script ?? undefined;
|
|
48
69
|
};
|
|
70
|
+
}
|
|
71
|
+
export function createBrowserPool(headless, reader, onPageError, onLeaseClosed, nonces) {
|
|
72
|
+
const maxContexts = resolveMaxContexts(process.env[ReticleEnv.MAX_CONTEXTS], cpus().length);
|
|
73
|
+
const genSessionId = () => `lease-${'function' === typeof globalThis.crypto?.randomUUID
|
|
74
|
+
? globalThis.crypto.randomUUID()
|
|
75
|
+
: String(Date.now())}`;
|
|
49
76
|
return new BrowserPool(playwrightLauncher({ headless }), {
|
|
50
77
|
maxContexts,
|
|
51
78
|
genSessionId,
|
|
52
|
-
zeroInstallScript,
|
|
79
|
+
zeroInstallScript: zeroInstallLoader(reader, nonces),
|
|
80
|
+
// The reader dials this origin; a lease off loopback adds it to the page CSP (#2016).
|
|
81
|
+
daemonOrigin: new URL(reader.url).origin,
|
|
53
82
|
launchHeaded: playwrightLauncher({ headless: false }),
|
|
83
|
+
...(onPageError === undefined ? {} : { onPageError }),
|
|
84
|
+
...(onLeaseClosed === undefined ? {} : { onLeaseClosed }),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Journal the uncaught page errors the pool hears into the session that connects for that lease.
|
|
89
|
+
*
|
|
90
|
+
* The pool sees every throw its pages make from before the first navigation; the SDK only exists
|
|
91
|
+
* afterwards. Without this wiring an import-time crash — a blank page with no error text anywhere
|
|
92
|
+
* (#1966) — was invisible to the agent driving it. Returns the journal so the pool's sink can be
|
|
93
|
+
* handed over; the bridge hook is registered here.
|
|
94
|
+
*/
|
|
95
|
+
export function attachPageErrorJournal(bridge) {
|
|
96
|
+
const journal = makePageErrorJournal({
|
|
97
|
+
resolveSession: (leaseId) => {
|
|
98
|
+
// Same resolution the lease tool uses: the lease id, or the id of a session whose URL
|
|
99
|
+
// carries the lease marker — an app that registered under its own name still reaches its lease.
|
|
100
|
+
const id = resolveLeasedSessionId(bridge.sessions, leaseId);
|
|
101
|
+
if (id === undefined) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const session = bridge.sessions.get(id);
|
|
105
|
+
return session === undefined ? undefined : session;
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
bridge.attachSessionCreate((session) => journal.onSessionCreate(session));
|
|
109
|
+
// The daemon runs for days; both of the journal's maps are keyed by things that end, so both
|
|
110
|
+
// endings are hooked. The bridge's end hook is additive — the durable journal's teardown runs too.
|
|
111
|
+
bridge.attachSessionEnd((session) => {
|
|
112
|
+
journal.onSessionEnd(session.id);
|
|
113
|
+
return Promise.resolve();
|
|
54
114
|
});
|
|
115
|
+
return journal;
|
|
55
116
|
}
|
|
56
117
|
/**
|
|
57
118
|
* Route CDP-authoritative network detail onto the driven session's journal.
|
|
@@ -98,7 +159,9 @@ export function makeNetworkDetailRouter(bridge, driveUrl) {
|
|
|
98
159
|
* so a failed launch never leaks a WS port. A past divergence between the two paths let daemon mode
|
|
99
160
|
* run with a different setup — this removes that risk.
|
|
100
161
|
*/
|
|
101
|
-
export async function resolveRealInput(options, onNavigateError, onNetworkDetail
|
|
162
|
+
export async function resolveRealInput(options, onNavigateError, onNetworkDetail,
|
|
163
|
+
/** Where a driven page with no SDK connects the zero-install reader. Absent: it gets none. */
|
|
164
|
+
reader) {
|
|
102
165
|
const driveUrl = options.driveUrl;
|
|
103
166
|
if (driveUrl !== undefined && driveUrl.length > 0) {
|
|
104
167
|
const headless = options.headless ?? true;
|
|
@@ -114,6 +177,7 @@ export async function resolveRealInput(options, onNavigateError, onNetworkDetail
|
|
|
114
177
|
...(injectConnect !== undefined ? { injectConnect } : {}),
|
|
115
178
|
...(storageState !== undefined ? { storageState } : {}),
|
|
116
179
|
...(onNetworkDetail !== undefined ? { onNetworkDetail } : {}),
|
|
180
|
+
...(reader !== undefined ? { zeroInstallScript: zeroInstallLoader(reader) } : {}),
|
|
117
181
|
}));
|
|
118
182
|
const launched = factory({ driveUrl, headless });
|
|
119
183
|
try {
|
|
@@ -5,6 +5,8 @@ import { localInitializeResponse, degradedInstructions, isHandshakeLine, drainLi
|
|
|
5
5
|
export { HandshakeReplay, RECONNECT_INITIALIZE_ID } from './proxy/proxy-handshake.js';
|
|
6
6
|
import { isToolCallRequest, isToolsListRequest, ToolCatalogCache } from './tool-catalog-cache.js';
|
|
7
7
|
import { rememberEnumerated, rememberProxyStarted, rememberToolCalled } from './attach-memory.js';
|
|
8
|
+
import { initializeClientName, rememberAgentConnected } from './peer/agents-connected.js';
|
|
9
|
+
import { agentClientOf } from '../../hooks/coding-agents.js';
|
|
8
10
|
import { toolsChangedNotification } from './proxy/proxy-handshake.js';
|
|
9
11
|
import { LOOPBACK_HOST, MCP_SSE_PATH, MCP_SHUTDOWN_EVENT, ReticleEnv, CONTRACT_FINGERPRINT, ProxyExitReason, PROXY_ID_PARAM, } from '@reticlehq/core';
|
|
10
12
|
import { SseFrameParser, buildSessionUrl } from './sse-frame-parser.js';
|
|
@@ -17,7 +19,8 @@ import { hasAnyProjectConnectedBefore } from '../../memory/recall/prior/connecti
|
|
|
17
19
|
import { projectIdsAt } from '../../command/cli/ports/resolve/cli-port.js';
|
|
18
20
|
import { PEER_VERSION_PARAM, PEER_CONTRACT_PARAM, agentSessionQuery, } from '../../command/version/peer-announce.js';
|
|
19
21
|
import { onStreamDrop, onClientRequest, onReconnectBudgetSpent, onQueueExpired, OnDrop, OnRequest, } from './proxy/proxy-lifecycle.js';
|
|
20
|
-
import {
|
|
22
|
+
import { probePresence } from '../../command/daemon/binding/port-presence.js';
|
|
23
|
+
import { describePortState } from '../../command/daemon/port-state.js';
|
|
21
24
|
import { PROXY_ID, flushProxyExit, noteProxyDrop, noteProxyToolCall, } from '../../telemetry/proxy-telemetry.js';
|
|
22
25
|
export { flushProxyExit, reportWakeRefused } from '../../telemetry/proxy-telemetry.js';
|
|
23
26
|
import { detectStack } from '../../telemetry/feedback-context.js';
|
|
@@ -513,7 +516,7 @@ boot) {
|
|
|
513
516
|
// The sentence `doctor` has always been able to say, in the log the agent's own process
|
|
514
517
|
// writes. A FOREIGN holder used to be indistinguishable from a free port here, and the
|
|
515
518
|
// proxy spun against it instead of saying which one it was.
|
|
516
|
-
const sentence =
|
|
519
|
+
const sentence = describePortState(presence, port);
|
|
517
520
|
proxyLog('reticle_mcp_proxy_dormant', { port, presence, note: sentence });
|
|
518
521
|
// Nothing is coming that could answer the client's handshake through this port, and the
|
|
519
522
|
// proxy now knows that rather than merely suspecting it. Waiting out LOCAL_HANDSHAKE_MS
|
|
@@ -792,6 +795,11 @@ boot) {
|
|
|
792
795
|
if (isToolsListRequest(trimmed)) {
|
|
793
796
|
rememberEnumerated(reticleStateHome(), port);
|
|
794
797
|
}
|
|
798
|
+
// WHICH agent connected, so the installer and `status` can say "Connected: Cursor ✓".
|
|
799
|
+
const client = agentClientOf(initializeClientName(trimmed));
|
|
800
|
+
if (client !== undefined) {
|
|
801
|
+
rememberAgentConnected(reticleStateHome(), client);
|
|
802
|
+
}
|
|
795
803
|
// The hop every other check misses. Recorded on the REQUEST rather than a reply, because
|
|
796
804
|
// the question is whether anything ever left the agent — a call that leaves and gets no
|
|
797
805
|
// answer is a different fault from one that was never made, and only this side sees it.
|
package/dist/surface/mcp/mcp.js
CHANGED
|
@@ -12,6 +12,7 @@ import { buildDynamicTools, descriptionOf } from '../tools/dynamic-tools.js';
|
|
|
12
12
|
import { runTool } from '../tools/invoke-tool.js';
|
|
13
13
|
import { sessionEnvelopeShape, newSnapshotCache } from '../tools/tool-kit.js';
|
|
14
14
|
import { buildErrorPayload, codedRefusalOf } from '../tools/error-recovery.js';
|
|
15
|
+
import { withRepeatHint } from '../tools/repeat-hint.js';
|
|
15
16
|
import { connectionSkew, takeVersionSkewOnto } from '../../command/version/version-nudge.js';
|
|
16
17
|
import { resultIsError } from './faults/mcp-is-error.js';
|
|
17
18
|
import { consumerVerdictRefusal, reservedVerdictKeysIn } from './consumer-verdict-guard.js';
|
|
@@ -149,8 +150,9 @@ function leanZodShape(shape, advertised, predicateAnchor) {
|
|
|
149
150
|
let hintSpent = predicateAnchor !== undefined;
|
|
150
151
|
for (const [key, schema] of Object.entries(shape)) {
|
|
151
152
|
if (isPredicateParam(schema)) {
|
|
152
|
-
// Every tool keeps the kinds; only the navigation hint is anchored.
|
|
153
|
-
|
|
153
|
+
// Every tool keeps the kinds; only the navigation hint is anchored. A bare string is visible
|
|
154
|
+
// text (core's `PredicateSchema`), so the SDK must let it through to that parse.
|
|
155
|
+
const compact = z.union([z.string(), z.record(z.string(), z.unknown())]);
|
|
154
156
|
out[key] = (schema.isOptional() ? compact.optional() : compact).describe(hintSpent ? PREDICATE_KINDS : COMPACT_PREDICATE_DESCRIPTION);
|
|
155
157
|
hintSpent = true;
|
|
156
158
|
continue;
|
|
@@ -769,7 +771,7 @@ tools = tableForSurface(profile)) {
|
|
|
769
771
|
content: [
|
|
770
772
|
{
|
|
771
773
|
type: 'text',
|
|
772
|
-
text: JSON.stringify(liveCallValues(takeVersionSkewOnto(buildErrorPayload(message, codedRefusalOf(error)), deps.peerSkew), advertisedNames)),
|
|
774
|
+
text: JSON.stringify(liveCallValues(takeVersionSkewOnto(withRepeatHint(buildErrorPayload(message, codedRefusalOf(error)), error), deps.peerSkew), advertisedNames)),
|
|
773
775
|
},
|
|
774
776
|
],
|
|
775
777
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The `AgentClient` values of every agent that has connected. Empty on a missing or broken file. */
|
|
2
|
+
export declare function connectedAgentClients(stateDir: string): ReadonlySet<string>;
|
|
3
|
+
/** Record that `client` (an `AgentClient` value) connected. Written once per client. */
|
|
4
|
+
export declare function rememberAgentConnected(stateDir: string, client: string): void;
|
|
5
|
+
/** `clientInfo.name` of an MCP `initialize` request line, or undefined for any other line. */
|
|
6
|
+
export declare function initializeClientName(line: string): string | undefined;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which coding agents have connected to Reticle on this machine, by name.
|
|
3
|
+
*
|
|
4
|
+
* `attach-memory.ts` records that SOME client attached on a port. It cannot say which one, so with
|
|
5
|
+
* two agents a healthy one hides a broken one. This record keeps the client that the MCP
|
|
6
|
+
* `initialize` handshake names, as its closed `AgentClient` value. It is machine wide, because the
|
|
7
|
+
* installer runs before there is a project and therefore before there is a port.
|
|
8
|
+
*
|
|
9
|
+
* The proxy writes it when the request arrives. It records a fact that happened, never an inference
|
|
10
|
+
* from a config file. Best-effort in both directions: a hint never breaks the client path.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
const RECORD_FILE = 'agents-connected.json';
|
|
15
|
+
const INITIALIZE_METHOD = 'initialize';
|
|
16
|
+
/** The `AgentClient` values of every agent that has connected. Empty on a missing or broken file. */
|
|
17
|
+
export function connectedAgentClients(stateDir) {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(readFileSync(join(stateDir, RECORD_FILE), 'utf8'));
|
|
20
|
+
return new Set(Array.isArray(parsed) ? parsed.filter((v) => 'string' === typeof v) : []);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return new Set();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Record that `client` (an `AgentClient` value) connected. Written once per client. */
|
|
27
|
+
export function rememberAgentConnected(stateDir, client) {
|
|
28
|
+
const known = connectedAgentClients(stateDir);
|
|
29
|
+
if (known.has(client)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(stateDir, { recursive: true });
|
|
34
|
+
writeFileSync(join(stateDir, RECORD_FILE), JSON.stringify([...known, client]), 'utf8');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// An unwritable state home is reported elsewhere; it must not break the client path.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** `clientInfo.name` of an MCP `initialize` request line, or undefined for any other line. */
|
|
41
|
+
export function initializeClientName(line) {
|
|
42
|
+
try {
|
|
43
|
+
const msg = JSON.parse(line);
|
|
44
|
+
if ('object' !== typeof msg || null === msg) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const { method, params } = msg;
|
|
48
|
+
if (INITIALIZE_METHOD !== method || 'object' !== typeof params || null === params) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const info = params.clientInfo;
|
|
52
|
+
const name = 'object' === typeof info && null !== info ? info.name : undefined;
|
|
53
|
+
return 'string' === typeof name && 0 < name.length ? name : undefined;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as net from 'node:net';
|
|
2
2
|
import { LOOPBACK_HOST } from '@reticlehq/core';
|
|
3
|
-
import {
|
|
3
|
+
import { presenceIsUsable, probePresence, PortPresence, } from '../../../command/daemon/binding/port-presence.js';
|
|
4
|
+
import { describePortState } from '../../../command/daemon/port-state.js';
|
|
4
5
|
import { fetchStatus } from '../../../command/daemon/binding/daemon-status-probe.js';
|
|
5
6
|
import { isAlive, readPid } from '../../../command/daemon/daemon.js';
|
|
6
7
|
const DEFAULT_DAEMON_READY_TIMEOUT_MS = 10_000;
|
|
@@ -21,6 +22,16 @@ function delay(ms) {
|
|
|
21
22
|
export function daemonPollDelayMs(attempt) {
|
|
22
23
|
return Math.min(DAEMON_POLL_INTERVAL_MS * attempt, DAEMON_POLL_MAX_INTERVAL_MS);
|
|
23
24
|
}
|
|
25
|
+
const CONNECTION_RESET = 'ECONNRESET';
|
|
26
|
+
/**
|
|
27
|
+
* A loopback connect to a port with no listener is refused at once. A timeout means a listener
|
|
28
|
+
* whose queue is full because it accepts nothing: a daemon stuck in its exit (#1937). Read as
|
|
29
|
+
* "free", that made `status` say the port was free while a process held it.
|
|
30
|
+
*
|
|
31
|
+
* Not on Windows: it retries a refused loopback connect for about two seconds, so there a timeout
|
|
32
|
+
* is also what a free port gives.
|
|
33
|
+
*/
|
|
34
|
+
const TIMEOUT_MEANS_HELD = 'win32' !== process.platform;
|
|
24
35
|
/**
|
|
25
36
|
* Returns true if something is already listening on the reticle port.
|
|
26
37
|
* Uses a plain TCP probe so we don't create a side-effectful SSE session
|
|
@@ -38,10 +49,11 @@ export function probeDaemon(port, host = LOOPBACK_HOST) {
|
|
|
38
49
|
socket.destroy();
|
|
39
50
|
resolve(true);
|
|
40
51
|
});
|
|
41
|
-
|
|
52
|
+
// A reset is an answer from a listener, not "nothing listens".
|
|
53
|
+
socket.on('error', (err) => resolve(CONNECTION_RESET === err.code));
|
|
42
54
|
socket.on('timeout', () => {
|
|
43
55
|
socket.destroy();
|
|
44
|
-
resolve(
|
|
56
|
+
resolve(TIMEOUT_MEANS_HELD);
|
|
45
57
|
});
|
|
46
58
|
socket.connect(port, host);
|
|
47
59
|
});
|
|
@@ -70,7 +82,7 @@ export async function waitForDaemon(port, probes = { tcpOpen: probeDaemon, statu
|
|
|
70
82
|
return;
|
|
71
83
|
if (false === probes.alive?.()) {
|
|
72
84
|
throw new Error(`the reticle daemon on port ${String(port)} exited before it was ready — ` +
|
|
73
|
-
|
|
85
|
+
describePortState(presence, port));
|
|
74
86
|
}
|
|
75
87
|
attempt++;
|
|
76
88
|
await delay(daemonPollDelayMs(attempt));
|
|
@@ -79,7 +91,7 @@ export async function waitForDaemon(port, probes = { tcpOpen: probeDaemon, statu
|
|
|
79
91
|
// means something is squatting the port. Reporting only the timeout sent readers hunting the
|
|
80
92
|
// wrong one.
|
|
81
93
|
throw new Error(`reticle daemon did not become ready on port ${String(port)} within ` +
|
|
82
|
-
`${String(DAEMON_READY_TIMEOUT_MS)}ms — ${
|
|
94
|
+
`${String(DAEMON_READY_TIMEOUT_MS)}ms — ${describePortState(presence, port)}`);
|
|
83
95
|
}
|
|
84
96
|
/** True while the process in the daemon pid file for `port` runs. */
|
|
85
97
|
export function daemonProcessAlive(port) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* outcome than "unknown".
|
|
12
12
|
*/
|
|
13
13
|
import { PredicateSchema } from '@reticlehq/engine/question/predicate/predicate-schema.js';
|
|
14
|
+
import { parsePredicate } from '@reticlehq/engine/question/predicate/predicate-parse.js';
|
|
14
15
|
import { SESSION_REF, isGlobalPressCall, unissuedRefRefusal } from '@reticlehq/core';
|
|
15
16
|
import { isDocumentGoneError } from '../../../portal/session/facts/session-replaced.js';
|
|
16
17
|
import { SessionReplacedError } from '../../../portal/session/page-commands/pending-commands.js';
|
|
@@ -77,8 +78,9 @@ const STEP_STRUCTURE_KEYS = new Set([
|
|
|
77
78
|
export function sequenceStepArgs(step) {
|
|
78
79
|
const flat = {};
|
|
79
80
|
for (const [key, value] of Object.entries(step)) {
|
|
80
|
-
if (!STEP_STRUCTURE_KEYS.has(key))
|
|
81
|
+
if (!STEP_STRUCTURE_KEYS.has(key)) {
|
|
81
82
|
flat[key] = value;
|
|
83
|
+
}
|
|
82
84
|
}
|
|
83
85
|
const nested = step['args'];
|
|
84
86
|
if ('string' === typeof nested) {
|
|
@@ -96,6 +98,22 @@ export function sequenceStepArgs(step) {
|
|
|
96
98
|
}
|
|
97
99
|
return { ...flat, ...(null !== nested && 'object' === typeof nested ? nested : {}) };
|
|
98
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Why `parsePredicate` refuses this `expect`, as the sentence `reticle_assert` gives.
|
|
103
|
+
*
|
|
104
|
+
* Called only after `PredicateSchema` has refused the value, so the parse throws. The schema stays
|
|
105
|
+
* the test for refusal: `parsePredicate` also refuses a value that the schema accepts, and this
|
|
106
|
+
* check must not refuse more than it did before.
|
|
107
|
+
*/
|
|
108
|
+
function expectParseIssue(value) {
|
|
109
|
+
try {
|
|
110
|
+
parsePredicate(value);
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return error instanceof Error ? error.message : String(error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
99
117
|
export function assertSequenceSteps(steps) {
|
|
100
118
|
if (0 === steps.length) {
|
|
101
119
|
throw new Error('reticle_act_sequence was given no steps. Pass steps: [{ ref, action, args? }] or [{ target, action, args? }] — nothing was acted on.');
|
|
@@ -118,18 +136,15 @@ export function assertSequenceSteps(steps) {
|
|
|
118
136
|
* declared; nothing told it the declaration was discarded, and that sentence invites it to add
|
|
119
137
|
* the very thing it just wrote. Same false green as an unreadable key, through a key this tool
|
|
120
138
|
* does read.
|
|
139
|
+
*
|
|
140
|
+
* The reason is the sentence `reticle_assert` gives for the same predicate: the field that failed
|
|
141
|
+
* and the fields that work. A fixed sentence with generic examples did not say that `css` is not a
|
|
142
|
+
* `query` field, so the caller had to guess again. That sentence also carries the schema's own
|
|
143
|
+
* words, such as the keys it saw when it could not tell the kind.
|
|
121
144
|
*/
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const said = parsed.error.issues
|
|
126
|
-
.filter((issue) => 'custom' === issue.code)
|
|
127
|
-
.map((issue) => ` ${issue.message}.`)
|
|
128
|
-
.join('');
|
|
129
|
-
throw new Error(`step ${String(i)} carries an \`expect\` this tool cannot parse.${said} ` +
|
|
130
|
-
'A consequence is a predicate object with a `kind` — e.g. { kind: "signal", name: ' +
|
|
131
|
-
'"order:placed" }, { kind: "element", by: "testid", value: "receipt" }, or ' +
|
|
132
|
-
'{ kind: "settled" } — the same shape reticle_act_and_wait takes as `until`. ' +
|
|
145
|
+
if (step['expect'] !== undefined && !PredicateSchema.safeParse(step['expect']).success) {
|
|
146
|
+
throw new Error(`step ${String(i)} carries an \`expect\` this tool cannot parse: ` +
|
|
147
|
+
`${expectParseIssue(step['expect'])} ` +
|
|
133
148
|
'Refused rather than dropped, because a step whose declaration was discarded reports ' +
|
|
134
149
|
'as a step that declared nothing. Nothing was acted on.');
|
|
135
150
|
}
|
|
@@ -145,10 +160,12 @@ export function assertSequenceSteps(steps) {
|
|
|
145
160
|
}
|
|
146
161
|
return;
|
|
147
162
|
}
|
|
148
|
-
if (step['target'] !== undefined)
|
|
163
|
+
if (step['target'] !== undefined) {
|
|
149
164
|
return;
|
|
150
|
-
|
|
165
|
+
}
|
|
166
|
+
if (isGlobalPressCall(step)) {
|
|
151
167
|
return;
|
|
168
|
+
}
|
|
152
169
|
throw new Error(`step ${String(i)} has no \`ref\` or \`target\`. ` +
|
|
153
170
|
'Sequence steps take `ref` (from reticle_query/reticle_snapshot) or `target` ' +
|
|
154
171
|
'(e.g. { testid } or { label }), the same locator reticle_act accepts. ' +
|
|
@@ -165,13 +182,15 @@ export function preflightAct(actArgs, until) {
|
|
|
165
182
|
// the agent its trusted click had happened. See act-danger.
|
|
166
183
|
assertNativeInputSupported(actArgs);
|
|
167
184
|
const unevaluable = unevaluablePredicateReason(until);
|
|
168
|
-
if (unevaluable !== undefined)
|
|
185
|
+
if (unevaluable !== undefined) {
|
|
169
186
|
throw new Error(unevaluable);
|
|
187
|
+
}
|
|
170
188
|
// A predicate that was already true when it was written answered `no-fault` / `already_true` —
|
|
171
189
|
// verdict-shaped, and carrying no proof of anything the action did. See vacuousPredicateReason.
|
|
172
190
|
const vacuous = vacuousPredicateReason(until);
|
|
173
|
-
if (vacuous !== undefined)
|
|
191
|
+
if (vacuous !== undefined) {
|
|
174
192
|
throw new Error(vacuous);
|
|
193
|
+
}
|
|
175
194
|
}
|
|
176
195
|
/**
|
|
177
196
|
* Dispatch a write, or report that its outcome went unobserved.
|
|
@@ -191,8 +210,9 @@ export async function dispatchAct(send) {
|
|
|
191
210
|
return await send();
|
|
192
211
|
}
|
|
193
212
|
catch (error) {
|
|
194
|
-
if (error instanceof SessionReplacedError || isDocumentGoneError(error))
|
|
213
|
+
if (error instanceof SessionReplacedError || isDocumentGoneError(error)) {
|
|
195
214
|
return null;
|
|
215
|
+
}
|
|
196
216
|
throw error;
|
|
197
217
|
}
|
|
198
218
|
}
|
|
@@ -48,7 +48,9 @@ export async function resolveActTarget(session, args,
|
|
|
48
48
|
* was ignored.
|
|
49
49
|
*/
|
|
50
50
|
appearanceMs) {
|
|
51
|
-
|
|
51
|
+
// A ref inside the target object (`target: { ref }`) is the same instruction as a top-level ref.
|
|
52
|
+
// Read as a query it named nothing, and the step failed with "Nothing was searched".
|
|
53
|
+
const ref = asString(args['ref']) ?? asString(asRecord(args['target'])['ref']);
|
|
52
54
|
if (ref !== undefined && ref.length > 0) {
|
|
53
55
|
// Refused here, at the boundary, and never re-read as a `target`: guessing what an arbitrary
|
|
54
56
|
// string meant is the silent re-resolution the stale-ref refusal exists to prevent.
|
|
@@ -9,9 +9,16 @@
|
|
|
9
9
|
* nothing on its own.
|
|
10
10
|
*/
|
|
11
11
|
import type { Predicate } from '@reticlehq/core';
|
|
12
|
+
import { evaluatePredicate, type EvalResult } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
12
13
|
import { waitForInFlight } from './settle-in-flight.js';
|
|
13
14
|
import { revertedAfterMatch } from './durable.js';
|
|
14
15
|
type Waited = Parameters<typeof waitForInFlight>[0] & Parameters<typeof revertedAfterMatch>[0];
|
|
15
16
|
/** Returns the sentence to fail with when the consequence reverted, else undefined. */
|
|
16
17
|
export declare function finishAfterMatch(session: Waited, until: Predicate, since: number, baselines: Parameters<typeof revertedAfterMatch>[3], remaining: () => number): Promise<string | undefined>;
|
|
18
|
+
/**
|
|
19
|
+
* A green that rests only on hidden matches, read again until a match shows, for at most
|
|
20
|
+
* SHOWN_GRACE_MS. Returns the reading that shows one, or `verdict` unchanged. Nothing is predicted:
|
|
21
|
+
* the verdict is judged on what the page painted, and a node that stays hidden stays `hidden_match`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function awaitShownMatch(session: Parameters<typeof evaluatePredicate>[0], until: Predicate, since: number, baselines: Parameters<typeof evaluatePredicate>[4], verdict: EvalResult, remaining: () => number): Promise<EvalResult>;
|
|
17
24
|
export {};
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* rest of the budget the caller already granted is spent here, in three steps, each deciding
|
|
9
9
|
* nothing on its own.
|
|
10
10
|
*/
|
|
11
|
+
import { hiddenMatchNote } from '@reticlehq/engine/evidence/already-true.js';
|
|
12
|
+
import { evaluatePredicate, } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
11
13
|
import { waitForInFlight } from './settle-in-flight.js';
|
|
12
14
|
import { waitForReaction } from './react-grace.js';
|
|
13
15
|
import { revertedAfterMatch } from './durable.js';
|
|
@@ -28,3 +30,34 @@ export async function finishAfterMatch(session, until, since, baselines, remaini
|
|
|
28
30
|
// 3. Then ask whether what held still holds — see revertedAfterMatch.
|
|
29
31
|
return revertedAfterMatch(session, until, since, baselines);
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* How long a match that holds only on hidden nodes gets to show. An entrance fade or a toast mounted
|
|
35
|
+
* at opacity 0 reads hidden for its first frame, and the wait matched on that frame. A node hidden
|
|
36
|
+
* by design pays this once, on a verdict that is `unknown` either way.
|
|
37
|
+
*/
|
|
38
|
+
const SHOWN_GRACE_MS = 1000;
|
|
39
|
+
/** Poll cadence — the same as the reaction grace. */
|
|
40
|
+
const SHOWN_POLL_MS = 50;
|
|
41
|
+
/**
|
|
42
|
+
* A green that rests only on hidden matches, read again until a match shows, for at most
|
|
43
|
+
* SHOWN_GRACE_MS. Returns the reading that shows one, or `verdict` unchanged. Nothing is predicted:
|
|
44
|
+
* the verdict is judged on what the page painted, and a node that stays hidden stays `hidden_match`.
|
|
45
|
+
*/
|
|
46
|
+
export async function awaitShownMatch(session, until, since, baselines, verdict, remaining) {
|
|
47
|
+
if (!verdict.pass || hiddenMatchNote(until, verdict.evidence) === undefined) {
|
|
48
|
+
return verdict;
|
|
49
|
+
}
|
|
50
|
+
// Counted in reads, not read off the session clock, so a clock that does not move still ends it.
|
|
51
|
+
const reads = Math.floor(Math.min(SHOWN_GRACE_MS, remaining()) / SHOWN_POLL_MS);
|
|
52
|
+
for (let read = 0; read < reads; read += 1) {
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, SHOWN_POLL_MS));
|
|
54
|
+
const now = await evaluatePredicate(session, until, since, true, baselines);
|
|
55
|
+
if (!now.pass) {
|
|
56
|
+
return verdict;
|
|
57
|
+
}
|
|
58
|
+
if (hiddenMatchNote(until, now.evidence) === undefined) {
|
|
59
|
+
return now;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return verdict;
|
|
63
|
+
}
|
|
@@ -9,6 +9,8 @@ export type TargetResolution = {
|
|
|
9
9
|
readonly kind: 'error';
|
|
10
10
|
readonly message: string;
|
|
11
11
|
};
|
|
12
|
+
/** The words before the nearest-target list in a no_match refusal. The repeat hint reads after it. */
|
|
13
|
+
export declare const NEAREST_TARGETS_LEAD = " The nearest targets on the page, most similar first: ";
|
|
12
14
|
/**
|
|
13
15
|
* Turn a target QUERY into the single ref an action can be performed on.
|
|
14
16
|
*
|