@reticlehq/server 3.8.2 → 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/network-mock.js +3 -2
- package/dist/portal/input/real-input.d.ts +15 -0
- package/dist/portal/input/real-input.js +59 -3
- 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 +10 -10
- 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/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/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- 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 +13 -9
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import * as production from '../../portal/production/production-activation.js';
|
|
13
13
|
import { z } from 'zod';
|
|
14
14
|
import { leaseNotConnectedHint, leaseReaderFailedHint, leaseStillLoadingHint, } from './lease-hint.js';
|
|
15
|
-
import { connectOrInject, moveReusedLease, notReadyReason, probeLeaseAlive, } from './lease-readiness.js';
|
|
15
|
+
import { LEASE_READY_TIMEOUT_MS, awaitLeaseAnswering, connectOrInject, connectWaitUntil, moveReusedLease, notReadyReason, probeLeaseAlive, } from './lease-readiness.js';
|
|
16
16
|
import { probeSdkMarker } from './gaps/sdk-marker-probe.js';
|
|
17
17
|
import { observeWebDocument } from '../../portal/session/dev-server/served-document.js';
|
|
18
18
|
import { diagnoseObservedWebCsp } from '@reticlehq/init';
|
|
@@ -25,7 +25,8 @@ import { LeaseNotReadyReason, RETICLE_URL_PARAM, RETICLE_DEFAULT_PORT, SeedStora
|
|
|
25
25
|
import { HudVisibility, ReticleTool, SESSION_HEALTH } from '@reticlehq/core';
|
|
26
26
|
import { HOST_STUB_NOTE } from './tool-kit.js';
|
|
27
27
|
import { PoolAtCapacityError } from '../../portal/pool/browser-pool.js';
|
|
28
|
-
import { asString } from '@reticlehq/core';
|
|
28
|
+
import { asNumber, asString } from '@reticlehq/core';
|
|
29
|
+
import { timeoutMsSchema } from './args/numeric-bounds.js';
|
|
29
30
|
import { chromiumPreflightRefusal } from '../../command/cli/doctor/browser/chromium-hint.js';
|
|
30
31
|
import { LEASE_PERMISSIONS_ARG, hintOf, notificationReadBack, parseLeasePermissions, permissionRefusal, refuseRegrant, } from './lease-permissions.js';
|
|
31
32
|
import { LEASE_EMULATE_ARG, leaseEmulationOf } from '../../portal/input/viewport-tools.js';
|
|
@@ -90,7 +91,7 @@ function alreadyHeldHint(sessionId, origin) {
|
|
|
90
91
|
return (`${ALREADY_HELD_LEASE} (${origin}): ${sessionId} — reused it rather than opening a second ` +
|
|
91
92
|
'tab, which would make every later call require sessionId. It is still signed in as whoever ' +
|
|
92
93
|
'used it last (cookies and storage come with it): release it first for a fresh context or ' +
|
|
93
|
-
'another identity.');
|
|
94
|
+
'another identity, or pass fresh: true to open a separate tab and keep this one.');
|
|
94
95
|
}
|
|
95
96
|
/**
|
|
96
97
|
* Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
|
|
@@ -259,6 +260,10 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
259
260
|
.optional()
|
|
260
261
|
.describe('Stable project id to stamp on the leased tab so the agent can scope to it.'),
|
|
261
262
|
seedStorage: SeedStorageSchema.optional().describe('Initial storage state (local, session and cookies) to seed before the first navigation (e.g. `seedStorage: { local: { token: "..." } }` to start already authenticated).'),
|
|
263
|
+
fresh: z
|
|
264
|
+
.boolean()
|
|
265
|
+
.optional()
|
|
266
|
+
.describe('Open a separate tab even when this connection already holds a lease on the origin, and keep that one. For parallel work that shares one MCP connection (sub-agents). Pass the returned sessionId to every later call.'),
|
|
262
267
|
initScript: z
|
|
263
268
|
.string()
|
|
264
269
|
.optional()
|
|
@@ -278,6 +283,9 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
278
283
|
permissions: LEASE_PERMISSIONS_ARG,
|
|
279
284
|
emulate: LEASE_EMULATE_ARG,
|
|
280
285
|
production: production.PRODUCTION_LEASE_ARG,
|
|
286
|
+
timeout_ms: timeoutMsSchema
|
|
287
|
+
.optional()
|
|
288
|
+
.describe(`How long to wait for the leased tab to connect and answer before answering ready:false with notReadyReason. Default ${String(LEASE_READY_TIMEOUT_MS)}. 0 looks once without waiting.`),
|
|
281
289
|
root: z
|
|
282
290
|
.string()
|
|
283
291
|
.optional()
|
|
@@ -288,7 +296,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
288
296
|
url: z.string(),
|
|
289
297
|
ready: z
|
|
290
298
|
.boolean()
|
|
291
|
-
.describe('Whether the leased tab is connected AND
|
|
299
|
+
.describe('Whether the leased tab is connected AND answered a probe under sessionId, so the next call finds it. acquire waits for this, up to timeout_ms. It is NOT a claim that the app has hydrated, so an act can miss: make the first act reticle_act_and_wait with an `until`, or reticle_assert { action: "wait" }. Empty reads may mean not hydrated. False ⇒ read notReadyReason.'),
|
|
292
300
|
zeroInstall: z
|
|
293
301
|
.boolean()
|
|
294
302
|
.optional()
|
|
@@ -297,6 +305,10 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
297
305
|
.string()
|
|
298
306
|
.optional()
|
|
299
307
|
.describe('Present when this leased tab runs an init script stubbing the bindings a desktop shell injects. Verdicts on this lease are about the app with that stub, not about the real shell.'),
|
|
308
|
+
cspWidenedFor: z
|
|
309
|
+
.string()
|
|
310
|
+
.optional()
|
|
311
|
+
.describe("Present when the lease added this origin (the daemon's WebSocket) to the page CSP connect-src, so the zero-install reader can connect. The rest of the page CSP applies as the server sent it."),
|
|
300
312
|
notReadyReason: z
|
|
301
313
|
.enum([
|
|
302
314
|
LeaseNotReadyReason.SDK_NEVER_DIALLED,
|
|
@@ -408,14 +420,17 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
408
420
|
// an init script has to exist before the page's own scripts run, which a loaded page cannot
|
|
409
421
|
// do, and the resolve below then finds nothing and mints a fresh context with the stub. A
|
|
410
422
|
// lease of the other kind is left alone, exactly as it is when a window was asked for.
|
|
411
|
-
|
|
423
|
+
// `fresh` asks for a separate tab: the held one is neither reused nor released (#1932).
|
|
424
|
+
const fresh = true === args['fresh'];
|
|
425
|
+
const heldOn = (o) => fresh || o === undefined ? undefined : pool.leaseIdOnOrigin(o, deps.attachId);
|
|
426
|
+
const heldForStub = heldOn(origin);
|
|
412
427
|
if (initScript !== undefined &&
|
|
413
428
|
heldForStub !== undefined &&
|
|
414
429
|
pool.isHeaded(heldForStub) === headed &&
|
|
415
430
|
pool.hostStubScript?.(heldForStub) !== initScript) {
|
|
416
431
|
await pool.release(heldForStub);
|
|
417
432
|
}
|
|
418
|
-
const held =
|
|
433
|
+
const held = heldOn(origin);
|
|
419
434
|
const existing = held !== undefined && pool.isHeaded(held) === headed ? held : undefined;
|
|
420
435
|
// A context's certificate setting is fixed when it is made, so an explicit request for the
|
|
421
436
|
// other one cannot be answered by the held lease: handing it back would keep checks off after
|
|
@@ -475,6 +490,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
475
490
|
// Asked of the pool, not remembered here: the stub was installed when THIS lease was
|
|
476
491
|
// minted, and the caller reusing it now may be seeing it for the first time.
|
|
477
492
|
...(true === pool.stubInstalled?.(existing) ? { hostStub: HOST_STUB_NOTE } : {}),
|
|
493
|
+
...cspWidenedOf(pool, existing),
|
|
478
494
|
...(alreadyOpen === undefined
|
|
479
495
|
? {}
|
|
480
496
|
: { preferExisting: { sessionId: alreadyOpen, note: PREFER_EXISTING_NOTE } }),
|
|
@@ -526,12 +542,18 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
526
542
|
// Resolved rather than assumed: an app that names its own session registers under that name,
|
|
527
543
|
// and the id we hand back has to be the one the agent can actually drive.
|
|
528
544
|
let registeredId;
|
|
529
|
-
const
|
|
530
|
-
const waited = await production.guardedForProduction(pool, lease, activated);
|
|
531
|
-
const { ready, zeroInstall, stillLoading, readerFailure } = await connectOrInject(waited, () => {
|
|
545
|
+
const isRegistered = () => {
|
|
532
546
|
registeredId = resolveLeasedSessionId(deps.sessions, lease.sessionId);
|
|
533
547
|
return registeredId !== undefined;
|
|
534
|
-
}
|
|
548
|
+
};
|
|
549
|
+
const waitStartedAt = deps.now();
|
|
550
|
+
const deadline = waitStartedAt + (asNumber(args['timeout_ms']) ?? LEASE_READY_TIMEOUT_MS);
|
|
551
|
+
const waited = await production.guardedForProduction(pool, lease, activated);
|
|
552
|
+
const { ready: connected, zeroInstall, stillLoading, readerFailure, } = await connectOrInject(waited, isRegistered, connectWaitUntil(deadline, deps.now));
|
|
553
|
+
// Connected is a row in the sessions map. Ready is a reply from it, so the next call works.
|
|
554
|
+
const answerReason = connected
|
|
555
|
+
? await awaitLeaseAnswering(() => (isRegistered() ? deps.sessions.get(registeredId ?? lease.sessionId) : undefined), deadline, deps.now)
|
|
556
|
+
: undefined;
|
|
535
557
|
// Tell the pool the other name this lease answers to. Every later touch and release arrives
|
|
536
558
|
// under the id we are about to hand back, and the pool is keyed by the id it navigated with;
|
|
537
559
|
// without this the touches miss, the lease ages out despite continuous activity, and the
|
|
@@ -541,6 +563,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
541
563
|
}
|
|
542
564
|
const effectiveSessionId = registeredId ?? lease.sessionId;
|
|
543
565
|
const session = deps.sessions.get(effectiveSessionId);
|
|
566
|
+
const ready = connected && answerReason === undefined;
|
|
544
567
|
if (validatedSeed !== undefined && session !== undefined) {
|
|
545
568
|
const failureReason = evaluateSeedPrecondition(url, session.url, validatedSeed, lease.navStatus);
|
|
546
569
|
if (failureReason !== undefined) {
|
|
@@ -552,18 +575,17 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
552
575
|
if (readerFailure === undefined) {
|
|
553
576
|
tellWatchers(deps, projectId, AGENT_DRIVING_ELSEWHERE);
|
|
554
577
|
}
|
|
555
|
-
// ready means the SDK
|
|
578
|
+
// ready means the SDK answers — not that contracts match. Carry the skew warning on acquire
|
|
556
579
|
// so the agent does not learn it only after a CDP tool invents a closed page (#688).
|
|
557
|
-
const mintReason = readerFailure?.reason ??
|
|
558
|
-
notReadyReason(ready, session, LeaseNotReadyReason.SDK_NEVER_DIALLED);
|
|
580
|
+
const mintReason = readerFailure?.reason ?? (connected ? answerReason : LeaseNotReadyReason.SDK_NEVER_DIALLED);
|
|
559
581
|
const versionSkew = registeredId === undefined ? undefined : deps.sessions.get(registeredId)?.versionSkew;
|
|
560
582
|
let notReadyHint;
|
|
561
|
-
if (!
|
|
583
|
+
if (!connected) {
|
|
562
584
|
if (readerFailure !== undefined) {
|
|
563
585
|
notReadyHint = leaseReaderFailedHint(url, readerFailure);
|
|
564
586
|
}
|
|
565
587
|
else if (true === stillLoading) {
|
|
566
|
-
notReadyHint = leaseStillLoadingHint(url,
|
|
588
|
+
notReadyHint = leaseStillLoadingHint(url, deps.now() - waitStartedAt);
|
|
567
589
|
}
|
|
568
590
|
else if (activated !== undefined) {
|
|
569
591
|
notReadyHint = production.notActivatedHint(url);
|
|
@@ -596,6 +618,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
596
618
|
...(versionSkew === undefined ? {} : { versionSkew }),
|
|
597
619
|
...production.summary(activated),
|
|
598
620
|
...(initScript === undefined ? {} : { hostStub: HOST_STUB_NOTE }),
|
|
621
|
+
...cspWidenedOf(pool, lease.sessionId),
|
|
599
622
|
...hintOf(notReadyHint, ready ? await notificationReadBack(pool, lease.sessionId, permissions) : undefined),
|
|
600
623
|
};
|
|
601
624
|
}
|
|
@@ -607,6 +630,11 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
607
630
|
}
|
|
608
631
|
},
|
|
609
632
|
};
|
|
633
|
+
/** `cspWidenedFor` when the lease changed its page CSP: a change to the page is declared (#2016). */
|
|
634
|
+
function cspWidenedOf(pool, leaseId) {
|
|
635
|
+
const origin = pool.cspWidenedFor?.(leaseId);
|
|
636
|
+
return origin === undefined ? {} : { cspWidenedFor: origin };
|
|
637
|
+
}
|
|
610
638
|
/** The whole not-connected diagnosis, gathered and worded. Split out so the acquire path stays flat. */
|
|
611
639
|
async function notConnectedHint(deps, url, dialledUrl) {
|
|
612
640
|
const bridgePort = deps.bridgePort ?? RETICLE_DEFAULT_PORT;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** The call in a streak that gets the hint. Two calls can be a deliberate retry; three is a loop. */
|
|
2
|
+
export declare const REPEAT_HINT_AT = 3;
|
|
3
|
+
export declare const RepeatHint: {
|
|
4
|
+
readonly NO_MATCH: string;
|
|
5
|
+
/** A no_match whose refusal listed the nearest targets: the retry is one of those refs. */
|
|
6
|
+
readonly noMatchNearest: (ref: string) => string;
|
|
7
|
+
readonly LOOK_SAME: string;
|
|
8
|
+
readonly RECOVERY: string;
|
|
9
|
+
readonly SAME: `You made this call ${string} times in a row with the same result.`;
|
|
10
|
+
/** A verdict repeated: name its own reason, the only thing the result says about why. */
|
|
11
|
+
readonly verdict: (verified: string, because: string | undefined) => string;
|
|
12
|
+
};
|
|
13
|
+
/** What a call came back with, as far as a repeat is concerned: an identity and the line to say. */
|
|
14
|
+
export interface RepeatOutcome {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly hint: string;
|
|
17
|
+
}
|
|
18
|
+
/** The outcome of a call that threw: its refusal reason. */
|
|
19
|
+
export declare function outcomeOfError(error: unknown): RepeatOutcome;
|
|
20
|
+
/** The outcome of a call that answered: its refusal reason, its verdict, or its whole answer. */
|
|
21
|
+
export declare function outcomeOfResult(tool: string, result: unknown): RepeatOutcome;
|
|
22
|
+
/** Tests only. */
|
|
23
|
+
export declare function resetRepeatHints(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Note one call of one connection, and answer the hint when this call is the third in a streak.
|
|
26
|
+
* `owner` is the agent connection; undefined (the Harness, the HUD, an embedding) is never counted.
|
|
27
|
+
*/
|
|
28
|
+
export declare function noteRepeat(owner: string | undefined, tool: string, args: Record<string, unknown>, outcome: RepeatOutcome): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Run one call of one connection inside its repeat streak. Answers the result and, on the third
|
|
31
|
+
* identical call, the hint. A call that throws keeps its hint beside the error (`withRepeatHint`).
|
|
32
|
+
*/
|
|
33
|
+
export declare function inRepeatStreak(owner: string | undefined, tool: string, args: Record<string, unknown>, run: () => Promise<unknown>): Promise<{
|
|
34
|
+
raw: unknown;
|
|
35
|
+
hint: string | undefined;
|
|
36
|
+
}>;
|
|
37
|
+
/** The answer with the hint beside it. Extra text only: the verdict and every field stay as they are. */
|
|
38
|
+
export declare function withHintOn(answer: unknown, hint: string | undefined): unknown;
|
|
39
|
+
/** The error payload with the hint its call earned, if any. */
|
|
40
|
+
export declare function withRepeatHint<T extends object>(payload: T, error: unknown): T;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One hint when an agent makes the same call, with the same result, for the third time in a row.
|
|
3
|
+
*
|
|
4
|
+
* Agents in the field made the same call five times or more in a row. Each answer was the same, and
|
|
5
|
+
* nothing in it said so. This module remembers the LAST call of each agent connection and gives one
|
|
6
|
+
* line, on the third identical call, that says what to try instead.
|
|
7
|
+
*
|
|
8
|
+
* Advice only: it reads a result that is already decided and never changes or blocks it. A hint says
|
|
9
|
+
* only what the result supports. When there is nothing specific to say, it says that the call was
|
|
10
|
+
* repeated with the same result.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { RefusalReason, ReticleTool, isPlainRecord } from '@reticlehq/core';
|
|
14
|
+
import { resultIsError } from '../mcp/faults/mcp-is-error.js';
|
|
15
|
+
import { buildErrorPayload, codedRefusalOf, refusalReasonFor } from './error-recovery.js';
|
|
16
|
+
import { EnvelopeKey } from './tool-kit.js';
|
|
17
|
+
import { NEAREST_TARGETS_LEAD } from './act/resolve-target.js';
|
|
18
|
+
/** The call in a streak that gets the hint. Two calls can be a deliberate retry; three is a loop. */
|
|
19
|
+
export const REPEAT_HINT_AT = 3;
|
|
20
|
+
const COUNT = String(REPEAT_HINT_AT);
|
|
21
|
+
export const RepeatHint = {
|
|
22
|
+
NO_MATCH: `You made this call ${COUNT} times and each time no element matched. Do not send it again. ` +
|
|
23
|
+
'Take one reticle_look { action: "find" } and use a ref from its answer.',
|
|
24
|
+
/** A no_match whose refusal listed the nearest targets: the retry is one of those refs. */
|
|
25
|
+
noMatchNearest: (ref) => `You made this call ${COUNT} times and each time no element matched. Do not send it again. ` +
|
|
26
|
+
`The refusal lists the nearest targets. Pass one of their refs as \`ref\`, for example "${ref}".`,
|
|
27
|
+
LOOK_SAME: `You made this look ${COUNT} times and each answer was the same, so the page did not change. ` +
|
|
28
|
+
'Act on the page, or wait for a change with reticle_assert { action: "wait" }.',
|
|
29
|
+
RECOVERY: `You made this call ${COUNT} times and it was refused for the same reason each time. ` +
|
|
30
|
+
'Do the step in `recovery` before you send it again.',
|
|
31
|
+
SAME: `You made this call ${COUNT} times in a row with the same result.`,
|
|
32
|
+
/** A verdict repeated: name its own reason, the only thing the result says about why. */
|
|
33
|
+
verdict: (verified, because) => `You made this call ${COUNT} times and each verdict was "${verified}".` +
|
|
34
|
+
(because === undefined ? '' : ` Its reason: "${because}".`) +
|
|
35
|
+
' The same call gives the same verdict. Change the target, the action or the `until`.',
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Result keys that change from call to call when nothing else did: the dispatcher's own advisories,
|
|
39
|
+
* the screenshot path, and the clock. A key that is missed here only means no hint fires.
|
|
40
|
+
*/
|
|
41
|
+
const VOLATILE_KEYS = new Set([
|
|
42
|
+
...Object.values(EnvelopeKey),
|
|
43
|
+
'cursor',
|
|
44
|
+
'since',
|
|
45
|
+
'at',
|
|
46
|
+
'ms',
|
|
47
|
+
'elapsedMs',
|
|
48
|
+
'durationMs',
|
|
49
|
+
]);
|
|
50
|
+
/** The same JSON for the same value, whatever the key order. */
|
|
51
|
+
function stableJson(value) {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return `[${value.map(stableJson).join(',')}]`;
|
|
54
|
+
}
|
|
55
|
+
if (isPlainRecord(value)) {
|
|
56
|
+
const keys = Object.keys(value)
|
|
57
|
+
.filter((key) => value[key] !== undefined)
|
|
58
|
+
.sort();
|
|
59
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
|
60
|
+
}
|
|
61
|
+
return JSON.stringify(value) ?? 'undefined';
|
|
62
|
+
}
|
|
63
|
+
/** A short digest, so a connection keeps a few bytes and not a whole page tree. */
|
|
64
|
+
function digest(text) {
|
|
65
|
+
return createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
66
|
+
}
|
|
67
|
+
/** The first ref in a no_match refusal's nearest-target list, or undefined when it has no list. */
|
|
68
|
+
function firstNearestRef(message) {
|
|
69
|
+
const at = message.indexOf(NEAREST_TARGETS_LEAD);
|
|
70
|
+
if (-1 === at) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return /^(\S+) \(/.exec(message.slice(at + NEAREST_TARGETS_LEAD.length))?.[1];
|
|
74
|
+
}
|
|
75
|
+
function refusal(reason, recovery, message) {
|
|
76
|
+
const id = `refused:${reason}`;
|
|
77
|
+
if (RefusalReason.NO_MATCH === reason) {
|
|
78
|
+
const ref = firstNearestRef(message);
|
|
79
|
+
return { id, hint: ref === undefined ? RepeatHint.NO_MATCH : RepeatHint.noMatchNearest(ref) };
|
|
80
|
+
}
|
|
81
|
+
return { id, hint: 'string' === typeof recovery ? RepeatHint.RECOVERY : RepeatHint.SAME };
|
|
82
|
+
}
|
|
83
|
+
/** The outcome of a call that threw: its refusal reason. */
|
|
84
|
+
export function outcomeOfError(error) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
const coded = codedRefusalOf(error);
|
|
87
|
+
return refusal(refusalReasonFor(message, coded), buildErrorPayload(message, coded).recovery, message);
|
|
88
|
+
}
|
|
89
|
+
/** The outcome of a call that answered: its refusal reason, its verdict, or its whole answer. */
|
|
90
|
+
export function outcomeOfResult(tool, result) {
|
|
91
|
+
if (!isPlainRecord(result)) {
|
|
92
|
+
return { id: `value:${digest(stableJson(result))}`, hint: RepeatHint.SAME };
|
|
93
|
+
}
|
|
94
|
+
if (resultIsError(result)) {
|
|
95
|
+
const message = String(result['error']);
|
|
96
|
+
return refusal(refusalReasonFor(message), result['recovery'], message);
|
|
97
|
+
}
|
|
98
|
+
if ('verified' in result) {
|
|
99
|
+
const verified = String(result['verified']);
|
|
100
|
+
const because = 'string' === typeof result['because'] ? result['because'] : undefined;
|
|
101
|
+
return {
|
|
102
|
+
id: `verdict:${verified}:${because ?? ''}`,
|
|
103
|
+
hint: RepeatHint.verdict(verified, because),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const kept = Object.fromEntries(Object.entries(result).filter(([key]) => !VOLATILE_KEYS.has(key)));
|
|
107
|
+
return {
|
|
108
|
+
id: `answer:${digest(stableJson(kept))}`,
|
|
109
|
+
hint: ReticleTool.LOOK === tool ? RepeatHint.LOOK_SAME : RepeatHint.SAME,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Bound on remembered connections: a daemon outlives them. Each keeps only its last call. */
|
|
113
|
+
const MAX_CONNECTIONS = 500;
|
|
114
|
+
const lastByConnection = new Map();
|
|
115
|
+
/** Tests only. */
|
|
116
|
+
export function resetRepeatHints() {
|
|
117
|
+
lastByConnection.clear();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Note one call of one connection, and answer the hint when this call is the third in a streak.
|
|
121
|
+
* `owner` is the agent connection; undefined (the Harness, the HUD, an embedding) is never counted.
|
|
122
|
+
*/
|
|
123
|
+
export function noteRepeat(owner, tool, args, outcome) {
|
|
124
|
+
if (owner === undefined) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const call = digest(`${tool}:${stableJson(args)}`);
|
|
128
|
+
const last = lastByConnection.get(owner);
|
|
129
|
+
const count = last?.call === call && last.outcome === outcome.id ? last.count + 1 : 1;
|
|
130
|
+
// Delete first, so the map order is last use and the oldest connection is the one that goes.
|
|
131
|
+
lastByConnection.delete(owner);
|
|
132
|
+
if (lastByConnection.size >= MAX_CONNECTIONS) {
|
|
133
|
+
const oldest = lastByConnection.keys().next().value;
|
|
134
|
+
if (oldest !== undefined) {
|
|
135
|
+
lastByConnection.delete(oldest);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
lastByConnection.set(owner, { call, outcome: outcome.id, count });
|
|
139
|
+
return REPEAT_HINT_AT === count ? outcome.hint : undefined;
|
|
140
|
+
}
|
|
141
|
+
/** Hints for calls that threw, kept beside the error so the boundary that answers can read them. */
|
|
142
|
+
const hintByError = new WeakMap();
|
|
143
|
+
/**
|
|
144
|
+
* Run one call of one connection inside its repeat streak. Answers the result and, on the third
|
|
145
|
+
* identical call, the hint. A call that throws keeps its hint beside the error (`withRepeatHint`).
|
|
146
|
+
*/
|
|
147
|
+
export async function inRepeatStreak(owner, tool, args, run) {
|
|
148
|
+
let raw;
|
|
149
|
+
try {
|
|
150
|
+
raw = await run();
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const hint = noteRepeat(owner, tool, args, outcomeOfError(error));
|
|
154
|
+
if (hint !== undefined && 'object' === typeof error && null !== error) {
|
|
155
|
+
hintByError.set(error, hint);
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
return { raw, hint: noteRepeat(owner, tool, args, outcomeOfResult(tool, raw)) };
|
|
160
|
+
}
|
|
161
|
+
/** The answer with the hint beside it. Extra text only: the verdict and every field stay as they are. */
|
|
162
|
+
export function withHintOn(answer, hint) {
|
|
163
|
+
return hint !== undefined && isPlainRecord(answer)
|
|
164
|
+
? { ...answer, [EnvelopeKey.REPEAT_HINT]: hint }
|
|
165
|
+
: answer;
|
|
166
|
+
}
|
|
167
|
+
/** The error payload with the hint its call earned, if any. */
|
|
168
|
+
export function withRepeatHint(payload, error) {
|
|
169
|
+
const hint = 'object' === typeof error && null !== error ? hintByError.get(error) : undefined;
|
|
170
|
+
return hint === undefined ? payload : { ...payload, [EnvelopeKey.REPEAT_HINT]: hint };
|
|
171
|
+
}
|
|
@@ -14,8 +14,8 @@ export declare function renderRunReport(report: RunReport): string;
|
|
|
14
14
|
export declare function endsRun(tool: string, args: Record<string, unknown>): boolean;
|
|
15
15
|
/** What an answer says the run saved or read. An answer that refused counts for nothing. */
|
|
16
16
|
export declare function noteToolResult(ledger: RunLedger, tool: string, args: Record<string, unknown>, result: unknown): void;
|
|
17
|
-
/** The report as an answer field, or no field when the
|
|
18
|
-
export declare function runReportField(ledger: RunLedger, root: string | undefined, env: Record<string, string | undefined>, now: number): Partial<Record<typeof RUN_REPORT_FIELD, {
|
|
17
|
+
/** The owner's report as an answer field, or no field when the owner has no run. Ends that run. */
|
|
18
|
+
export declare function runReportField(ledger: RunLedger, root: string | undefined, env: Record<string, string | undefined>, now: number, owner?: string): Partial<Record<typeof RUN_REPORT_FIELD, {
|
|
19
19
|
report: RunReport;
|
|
20
20
|
markdown: string;
|
|
21
21
|
instruction: string;
|
|
@@ -34,12 +34,15 @@ function headline(report) {
|
|
|
34
34
|
const count = (v) => all.filter((each) => v === each).length;
|
|
35
35
|
const total = all.length;
|
|
36
36
|
const word = `${HEAD}${WORD[report.verified]}**${SEP}`;
|
|
37
|
-
if (0 === total)
|
|
37
|
+
if (0 === total) {
|
|
38
38
|
return `${word}${Said.NOTHING_CHECKED}`;
|
|
39
|
-
|
|
39
|
+
}
|
|
40
|
+
if (Verified.NO === report.verified) {
|
|
40
41
|
return `${word}${String(count(Verified.NO))} of ${plural(total, 'check')} did not hold`;
|
|
41
|
-
|
|
42
|
+
}
|
|
43
|
+
if (Verified.YES === report.verified) {
|
|
42
44
|
return `${word}${String(total)} of ${plural(total, 'check')} held`;
|
|
45
|
+
}
|
|
43
46
|
return `${word}${String(total - count(Verified.YES) - count(Verified.NO))} of ${plural(total, 'check')} undecided`;
|
|
44
47
|
}
|
|
45
48
|
/** The report as markdown: the verdict, each claim with its evidence, then what the run left. */
|
|
@@ -95,39 +98,48 @@ const INTENT_READS = new Set(['list', 'index', 'subject', 'get']);
|
|
|
95
98
|
const isRecord = (value) => null !== value && 'object' === typeof value && !Array.isArray(value);
|
|
96
99
|
/** What an answer says the run saved or read. An answer that refused counts for nothing. */
|
|
97
100
|
export function noteToolResult(ledger, tool, args, result) {
|
|
98
|
-
if (!isRecord(result) || result['error'] !== undefined)
|
|
101
|
+
if (!isRecord(result) || result['error'] !== undefined) {
|
|
99
102
|
return;
|
|
103
|
+
}
|
|
100
104
|
if (ReticleTool.FLOW_SAVE === tool || ReticleTool.FLOW_SAVE_RECORDED === tool) {
|
|
101
105
|
const name = result['name'];
|
|
102
|
-
if ('string' === typeof name)
|
|
106
|
+
if ('string' === typeof name) {
|
|
103
107
|
ledger.flowSaved(name);
|
|
108
|
+
}
|
|
104
109
|
return;
|
|
105
110
|
}
|
|
106
111
|
const action = asString(args['action']);
|
|
107
112
|
if (ReticleTool.INTENT === tool) {
|
|
108
113
|
// The intent ledger in `.reticle/intent/` is the memory an unlinked project has.
|
|
109
|
-
if (IntentWrite.RECORD === action)
|
|
114
|
+
if (IntentWrite.RECORD === action) {
|
|
110
115
|
ledger.memoryWritten();
|
|
116
|
+
}
|
|
111
117
|
const declared = args['intents'];
|
|
112
|
-
if (IntentWrite.DECLARE === action && Array.isArray(declared))
|
|
118
|
+
if (IntentWrite.DECLARE === action && Array.isArray(declared)) {
|
|
113
119
|
ledger.memoryWritten(declared.length);
|
|
114
|
-
|
|
120
|
+
}
|
|
121
|
+
if (action !== undefined && INTENT_READS.has(action)) {
|
|
115
122
|
ledger.memoryRead([JSON.stringify(result)]);
|
|
123
|
+
}
|
|
116
124
|
return;
|
|
117
125
|
}
|
|
118
|
-
if (ReticleTool.MEMORY !== tool)
|
|
126
|
+
if (ReticleTool.MEMORY !== tool) {
|
|
119
127
|
return;
|
|
120
|
-
|
|
128
|
+
}
|
|
129
|
+
if ('add' === action && result['added'] !== undefined) {
|
|
121
130
|
ledger.memoryWritten();
|
|
131
|
+
}
|
|
122
132
|
const known = result['known'];
|
|
123
|
-
if (Array.isArray(known))
|
|
133
|
+
if (Array.isArray(known)) {
|
|
124
134
|
ledger.memoryRead(known.map((entry) => JSON.stringify(entry)));
|
|
135
|
+
}
|
|
125
136
|
}
|
|
126
|
-
/** The report as an answer field, or no field when the
|
|
127
|
-
export function runReportField(ledger, root, env, now) {
|
|
128
|
-
if (ledger.isEmpty())
|
|
137
|
+
/** The owner's report as an answer field, or no field when the owner has no run. Ends that run. */
|
|
138
|
+
export function runReportField(ledger, root, env, now, owner) {
|
|
139
|
+
if (ledger.isEmpty(owner)) {
|
|
129
140
|
return {};
|
|
130
|
-
|
|
141
|
+
}
|
|
142
|
+
const taken = ledger.take(now, owner);
|
|
131
143
|
const silenced = env[UPGRADE_HINT_SILENCE_ENV] !== undefined;
|
|
132
144
|
const linked = root === undefined || linkedToPlatform(root, env);
|
|
133
145
|
const report = silenced || linked ? taken : { ...taken, cloud: CLOUD_VALUE_LINE };
|
|
@@ -96,6 +96,11 @@ export interface ToolDeps<Ext = unknown> {
|
|
|
96
96
|
} | undefined>;
|
|
97
97
|
/** The one dev server running in the caller's project folder, for a lease that names no url. */
|
|
98
98
|
projectAppUrl?: () => Promise<string | undefined>;
|
|
99
|
+
/**
|
|
100
|
+
* The first-session offer for this attach id's first call, when no app is attached and the
|
|
101
|
+
* project is not wired (see zero-install-first-call.ts). Absent: no offer.
|
|
102
|
+
*/
|
|
103
|
+
firstSessionOffer?: (attachId: string) => string | undefined;
|
|
99
104
|
/** cross-run outcome memory (.reticle/project.json). */
|
|
100
105
|
project: ProjectStore;
|
|
101
106
|
/** optional native-input provider. undefined ⇒ everything stays synthetic. */
|
|
@@ -303,10 +308,17 @@ export declare const EnvelopeKey: {
|
|
|
303
308
|
* what it cannot see, and the command that wires the app for good. See zero-install-first-call.ts.
|
|
304
309
|
*/
|
|
305
310
|
readonly ZERO_INSTALL: "zero_install";
|
|
311
|
+
/**
|
|
312
|
+
* Once per agent connection, on its first call: this unwired web app can be checked now with the
|
|
313
|
+
* lease, and `init` needs the user's consent. See zero-install-first-call.ts.
|
|
314
|
+
*/
|
|
315
|
+
readonly FIRST_SESSION_OFFER: "first_session_offer";
|
|
306
316
|
/** Once-per-session advisory lines an agent's own call carries. See agent/notices.ts. */
|
|
307
317
|
readonly NOTICES: "notices";
|
|
308
318
|
/** The end-of-run report, on the call that ends an agent's run. See run-report.ts. */
|
|
309
319
|
readonly RUN_REPORT: "runReport";
|
|
320
|
+
/** On the third identical call with the same result: what to try instead. See repeat-hint.ts. */
|
|
321
|
+
readonly REPEAT_HINT: "repeatHint";
|
|
310
322
|
};
|
|
311
323
|
export type EnvelopeKey = (typeof EnvelopeKey)[keyof typeof EnvelopeKey];
|
|
312
324
|
/**
|
|
@@ -99,10 +99,17 @@ export const EnvelopeKey = {
|
|
|
99
99
|
* what it cannot see, and the command that wires the app for good. See zero-install-first-call.ts.
|
|
100
100
|
*/
|
|
101
101
|
ZERO_INSTALL: 'zero_install',
|
|
102
|
+
/**
|
|
103
|
+
* Once per agent connection, on its first call: this unwired web app can be checked now with the
|
|
104
|
+
* lease, and `init` needs the user's consent. See zero-install-first-call.ts.
|
|
105
|
+
*/
|
|
106
|
+
FIRST_SESSION_OFFER: 'first_session_offer',
|
|
102
107
|
/** Once-per-session advisory lines an agent's own call carries. See agent/notices.ts. */
|
|
103
108
|
NOTICES: 'notices',
|
|
104
109
|
/** The end-of-run report, on the call that ends an agent's run. See run-report.ts. */
|
|
105
110
|
RUN_REPORT: RUN_REPORT_FIELD,
|
|
111
|
+
/** On the third identical call with the same result: what to try instead. See repeat-hint.ts. */
|
|
112
|
+
REPEAT_HINT: 'repeatHint',
|
|
106
113
|
};
|
|
107
114
|
/**
|
|
108
115
|
* The sentence every session-bound result carries while its leased tab runs on a stubbed shell.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TESTID_FOUND_UNDER_SCHEMA } from './query-hint-schema.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { EMPTY_SESSION_LIST_OUTPUT, emptySessionList, nextStepBesideLiveSessions, } from './empty-session-list.js';
|
|
4
|
-
import { AttrNamesSchema, QueryBy, ReticleCommand, SnapshotMode } from '@reticlehq/core';
|
|
3
|
+
import { EMPTY_SESSION_LIST_OUTPUT, emptySessionList, NEXT_STEP_FIELD, nextStepBesideLiveSessions, } from './empty-session-list.js';
|
|
4
|
+
import { AttrNamesSchema, NearestTargetSchema, QueryBy, ReticleCommand, SnapshotMode, } from '@reticlehq/core';
|
|
5
5
|
import { ReticleTool } from '@reticlehq/core';
|
|
6
6
|
import { withSizeCost } from '../../portal/session/output-budget.js';
|
|
7
7
|
import { applySnapshotDelta, SnapshotCache } from './read/snapshot-delta.js';
|
|
@@ -145,11 +145,12 @@ export const RAW_TOOLS = withCompactEnvelope([
|
|
|
145
145
|
})));
|
|
146
146
|
const sessions = await list();
|
|
147
147
|
// An empty list is a dead end on its own; see empty-session-list.ts for what it carries.
|
|
148
|
-
if (0 === sessions.length)
|
|
148
|
+
if (0 === sessions.length) {
|
|
149
149
|
return emptySessionList(deps, sessions);
|
|
150
|
+
}
|
|
150
151
|
// Another project's tab is up. This caller's project may still never have been wired.
|
|
151
152
|
const nextStep = nextStepBesideLiveSessions(deps, sessions);
|
|
152
|
-
return nextStep === undefined ? { sessions } : { sessions,
|
|
153
|
+
return nextStep === undefined ? { sessions } : { sessions, [NEXT_STEP_FIELD]: nextStep };
|
|
153
154
|
},
|
|
154
155
|
},
|
|
155
156
|
{
|
|
@@ -284,7 +285,7 @@ export const RAW_TOOLS = withCompactEnvelope([
|
|
|
284
285
|
name: z
|
|
285
286
|
.string()
|
|
286
287
|
.optional()
|
|
287
|
-
.describe('
|
|
288
|
+
.describe('Exact accessible-name filter - narrows results when `by` is role and the page has many elements of that role. Matches the whole name; use `text` for a partial match.'),
|
|
288
289
|
scope: z
|
|
289
290
|
.string()
|
|
290
291
|
.optional()
|
|
@@ -398,6 +399,10 @@ export const RAW_TOOLS = withCompactEnvelope([
|
|
|
398
399
|
.optional()
|
|
399
400
|
.describe('Present when a ROLE+NAME search missed and that role DOES carry a nearly-matching name — role+name is exact, so "Mesh" does not find "2 Mesh". Retry with one of these spellings; no snapshot needed.'),
|
|
400
401
|
testidFoundUnder: TESTID_FOUND_UNDER_SCHEMA,
|
|
402
|
+
nearest: z
|
|
403
|
+
.array(NearestTargetSchema)
|
|
404
|
+
.optional()
|
|
405
|
+
.describe('Present when a search missed and the page has visible targets like the one asked for: up to 3, the asked role first, then by name similarity. Each has a ref to retry with. nameTruncated is true when the name was cut. Advice only, never a match.'),
|
|
401
406
|
})
|
|
402
407
|
.optional()
|
|
403
408
|
.describe('Present only on zero matches — tells you what IS on the page so you can diagnose the miss.'),
|
|
@@ -422,8 +427,9 @@ export const RAW_TOOLS = withCompactEnvelope([
|
|
|
422
427
|
// Accept the predicate's named-field spelling too — see query-shape.ts.
|
|
423
428
|
const args = normalizeQueryArgs(rawArgs);
|
|
424
429
|
const empty = emptyQueryRefusal(args);
|
|
425
|
-
if (empty !== undefined)
|
|
430
|
+
if (empty !== undefined) {
|
|
426
431
|
return Promise.reject(empty);
|
|
432
|
+
}
|
|
427
433
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
428
434
|
return commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.QUERY, {
|
|
429
435
|
by: args['by'],
|
|
@@ -603,14 +609,17 @@ export const RETIRED_FROM_SURFACE = [
|
|
|
603
609
|
* The browser counts what leanness passed over, so the tool can say which of the two it is.
|
|
604
610
|
*/
|
|
605
611
|
function noteEmptyLeanTree(result, mode) {
|
|
606
|
-
if (SnapshotMode.INTERACTIVE !== mode)
|
|
612
|
+
if (SnapshotMode.INTERACTIVE !== mode) {
|
|
607
613
|
return result;
|
|
614
|
+
}
|
|
608
615
|
const row = asRecord(result);
|
|
609
|
-
if (0 !== asNumber(row['nodes']))
|
|
616
|
+
if (0 !== asNumber(row['nodes'])) {
|
|
610
617
|
return result;
|
|
618
|
+
}
|
|
611
619
|
const skipped = asNumber(row['leanSkipped']) ?? 0;
|
|
612
|
-
if (0 === skipped)
|
|
620
|
+
if (0 === skipped) {
|
|
613
621
|
return result;
|
|
622
|
+
}
|
|
614
623
|
return {
|
|
615
624
|
...row,
|
|
616
625
|
note: `no element on this page carries an interactive ARIA role, so this mode found nothing — ` +
|
|
@@ -635,18 +644,22 @@ function noteEmptyLeanTree(result, mode) {
|
|
|
635
644
|
* empty tree it replaces.
|
|
636
645
|
*/
|
|
637
646
|
function noteHiddenPage(result, mode) {
|
|
638
|
-
if (SnapshotMode.STATUS === mode)
|
|
647
|
+
if (SnapshotMode.STATUS === mode) {
|
|
639
648
|
return result;
|
|
649
|
+
}
|
|
640
650
|
const row = asRecord(result);
|
|
641
|
-
if (0 !== asNumber(row['nodes']))
|
|
651
|
+
if (0 !== asNumber(row['nodes'])) {
|
|
642
652
|
return result;
|
|
653
|
+
}
|
|
643
654
|
// A note already there is the more specific one (leanness), and two explanations for one empty
|
|
644
655
|
// tree is worse than the better of them alone.
|
|
645
|
-
if (row['note'] !== undefined)
|
|
656
|
+
if (row['note'] !== undefined) {
|
|
646
657
|
return result;
|
|
658
|
+
}
|
|
647
659
|
const skipped = asNumber(row['hiddenSkipped']) ?? 0;
|
|
648
|
-
if (0 === skipped)
|
|
660
|
+
if (0 === skipped) {
|
|
649
661
|
return result;
|
|
662
|
+
}
|
|
650
663
|
return {
|
|
651
664
|
...row,
|
|
652
665
|
note: `this is NOT an empty page: ${String(skipped)} subtree(s) were skipped because their root ` +
|