@reticlehq/server 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +16 -13
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/cli-doctor.js +25 -5
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +7 -1
- package/dist/command/cli/cli-parse.js +33 -5
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +129 -17
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +1 -1
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/launch/cli-launch.d.ts +1 -29
- package/dist/command/cli/launch/cli-launch.js +6 -40
- package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
- package/dist/command/cli/ports/resolve/cli-port.js +45 -1
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli.js +5 -0
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +1 -0
- package/dist/features/harness/jev-driver.js +6 -9
- package/dist/features/harness/platform-config.d.ts +10 -0
- package/dist/features/harness/platform-config.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +40 -95
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +31 -9
- package/dist/language/flows/flow-replay-run.js +146 -33
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +12 -11
- package/dist/language/flows/flow-replay.js +79 -109
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flows.d.ts +8 -4
- package/dist/language/flows/flows.js +11 -2
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +12 -8
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/verify-change-tools.js +14 -0
- package/dist/memory/cloud/cloud-sync.d.ts +0 -2
- package/dist/memory/cloud/cloud-sync.js +1 -1
- package/dist/memory/cloud/harness-config.d.ts +2 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +13 -0
- package/dist/memory/cloud/sync-daemon.js +21 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-store.js +6 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +252 -140
- package/dist/memory/intent/intent-shard.d.ts +10 -11
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-recorder.d.ts +21 -0
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +14 -10
- package/dist/memory/journal/on-disk/retention.js +13 -12
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/session-end.d.ts +4 -1
- package/dist/memory/journal/session-end.js +64 -6
- package/dist/memory/journal/session-journal.d.ts +8 -1
- package/dist/memory/journal/session-journal.js +10 -0
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
- package/dist/memory/project/fs/fs-port.js +44 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.js +5 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +9 -0
- package/dist/portal/session/session-manager.js +23 -2
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +19 -7
- package/dist/surface/mcp/mcp-proxy.js +3 -3
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +22 -7
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +30 -4
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/dynamic-tools.js +6 -0
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +17 -4
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/observe-tools.js +14 -6
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +5 -0
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools.js +64 -29
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/cli/cloud.mdx +9 -7
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/desktop.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +128 -4
- package/docs/tools/query.mdx +10 -10
- package/docs/usage.md +1 -1
- package/docs/what-is-recorded.md +62 -0
- package/package.json +6 -6
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* - still in flight when the budget genuinely runs out → `unsettled`, as before, but now an honest
|
|
28
28
|
* limit instead of an early exit.
|
|
29
29
|
*/
|
|
30
|
-
import { EventType, NetInitiator, isDevToolingUrl } from '@reticlehq/core';
|
|
30
|
+
import { EventType, NetInitiator, isDevToolingUrl, isForeignTraffic } from '@reticlehq/core';
|
|
31
31
|
/**
|
|
32
32
|
* The dev toolchain talking about ITSELF is not the app finishing its work.
|
|
33
33
|
*
|
|
@@ -37,6 +37,25 @@ import { EventType, NetInitiator, isDevToolingUrl } from '@reticlehq/core';
|
|
|
37
37
|
* was graded on the strength of a Next webpack hot-update the same payload printed as ignored.
|
|
38
38
|
*/
|
|
39
39
|
const isDevTooling = (data) => isDevToolingUrl('string' === typeof data['url'] ? data['url'] : undefined);
|
|
40
|
+
/**
|
|
41
|
+
* Somebody else's host is not the app finishing its work.
|
|
42
|
+
*
|
|
43
|
+
* The comment above makes this argument for dev tooling and it holds identically here:
|
|
44
|
+
* `splitForeignTraffic` drops dev tooling AND third-party traffic, and this file dropped only the
|
|
45
|
+
* first — so the exclusion stayed cosmetic exactly where it decides a verdict, for the other half.
|
|
46
|
+
*
|
|
47
|
+
* Reported from the field repeatedly: an app embedding a wallet SDK that continuously POSTs
|
|
48
|
+
* telemetry to its vendor host never settles, so EVERY assertion on that app returns
|
|
49
|
+
* `unknown / outcome_pending` whatever the feature under test was. A third-party beacon cannot
|
|
50
|
+
* answer "has the app finished?", and nothing makes it stop, so waiting for it is waiting forever.
|
|
51
|
+
*
|
|
52
|
+
* Fails OPEN: with no known app origin nothing is foreign, because suppressing a request we cannot
|
|
53
|
+
* classify is how a verdict starts resting on evidence it discarded.
|
|
54
|
+
*
|
|
55
|
+
* SAME-ORIGIN background traffic is never guessed: nothing in a URL separates the app's telemetry
|
|
56
|
+
* from the app's work. It is excluded only when the project DECLARED it (`background`).
|
|
57
|
+
*/
|
|
58
|
+
const isForeign = (data, appOrigin, background) => isForeignTraffic('string' === typeof data['url'] ? data['url'] : undefined, appOrigin, background);
|
|
40
59
|
/**
|
|
41
60
|
* A departure records where the browser was SENT, not a request whose result we will see — the
|
|
42
61
|
* response is delivered to a document this session does not live into. It can never be matched by a
|
|
@@ -54,7 +73,7 @@ const idOf = (data) => 'string' === typeof data['id'] ? data['id'] : undefined;
|
|
|
54
73
|
* the act cursor, so a response belonging to an earlier action can land inside it, and counting that
|
|
55
74
|
* as progress would let unrelated traffic close the window early.
|
|
56
75
|
*/
|
|
57
|
-
export function inFlightRequestIds(events) {
|
|
76
|
+
export function inFlightRequestIds(events, appOrigin, background = []) {
|
|
58
77
|
const settled = new Set();
|
|
59
78
|
for (const e of events) {
|
|
60
79
|
if (e.type !== EventType.NET_REQUEST)
|
|
@@ -67,6 +86,8 @@ export function inFlightRequestIds(events) {
|
|
|
67
86
|
for (const e of events) {
|
|
68
87
|
if (e.type !== EventType.NET_PENDING || isDevTooling(e.data) || isDeparture(e.data))
|
|
69
88
|
continue;
|
|
89
|
+
if (isForeign(e.data, appOrigin, background))
|
|
90
|
+
continue;
|
|
70
91
|
const id = idOf(e.data);
|
|
71
92
|
if (id !== undefined && !settled.has(id) && !open.includes(id))
|
|
72
93
|
open.push(id);
|
|
@@ -81,8 +102,8 @@ export function inFlightRequestIds(events) {
|
|
|
81
102
|
* directly. Read off the NET_PENDING the window already holds, so this costs one pass over events
|
|
82
103
|
* that are in hand.
|
|
83
104
|
*/
|
|
84
|
-
export function inFlightRequestLabels(events) {
|
|
85
|
-
const open = new Set(inFlightRequestIds(events));
|
|
105
|
+
export function inFlightRequestLabels(events, appOrigin, background = []) {
|
|
106
|
+
const open = new Set(inFlightRequestIds(events, appOrigin, background));
|
|
86
107
|
const labels = [];
|
|
87
108
|
for (const e of events) {
|
|
88
109
|
if (e.type !== EventType.NET_PENDING)
|
|
@@ -136,7 +157,9 @@ export function repeatedRequestLabels(events) {
|
|
|
136
157
|
* timeout reaching the predicate, and a negative deadline must mean "stop", never "loop forever".
|
|
137
158
|
*/
|
|
138
159
|
export async function waitForInFlight(session, since, budgetMs, opts) {
|
|
139
|
-
|
|
160
|
+
// The verdict's own scope: waiting on a request the verdict will not count is latency for nothing.
|
|
161
|
+
const outstanding = () => inFlightRequestIds(session.eventsSince(since), session.url, session.background);
|
|
162
|
+
let open = outstanding();
|
|
140
163
|
if (0 === open.length)
|
|
141
164
|
return { settled: true, stillInFlight: [] };
|
|
142
165
|
if (budgetMs <= 0)
|
|
@@ -145,7 +168,7 @@ export async function waitForInFlight(session, since, budgetMs, opts) {
|
|
|
145
168
|
while (session.elapsed() < deadline) {
|
|
146
169
|
// Never overshoot the budget: the last sleep is trimmed to whatever is actually left.
|
|
147
170
|
await opts.sleep(Math.min(POLL_MS, deadline - session.elapsed()));
|
|
148
|
-
open =
|
|
171
|
+
open = outstanding();
|
|
149
172
|
if (0 === open.length)
|
|
150
173
|
return { settled: true, stillInFlight: [] };
|
|
151
174
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* must never fail the run that found it.
|
|
11
11
|
*/
|
|
12
12
|
import { ActionType, AnchorKind } from '@reticlehq/core';
|
|
13
|
-
import { CapsuleStore, capsuleId, CAPSULE_VERSION } from '../../judgement/capsule/capsule-store.js';
|
|
13
|
+
import { CapsuleStore, capsuleFingerprint, capsuleId, CAPSULE_VERSION, } from '../../judgement/capsule/capsule-store.js';
|
|
14
14
|
import { ReticleTool } from '@reticlehq/core';
|
|
15
15
|
import { asRecord, asString } from '@reticlehq/core';
|
|
16
16
|
import { sessionRoot } from '../../memory/project/session-root.js';
|
|
@@ -19,7 +19,6 @@ export async function saveFailedAssertCapsule(inputs) {
|
|
|
19
19
|
const { deps, verdict, capsule, links, args, actResult, actedSource, root: given } = inputs;
|
|
20
20
|
if (verdict.pass || capsule === undefined)
|
|
21
21
|
return undefined;
|
|
22
|
-
const id = capsuleId(deps.now(), asString(args['ref']) ?? 'assert');
|
|
23
22
|
const expectedText = links
|
|
24
23
|
.map((l) => ('name' in l ? `${l.kind} ${String(l.name)}` : l.kind))
|
|
25
24
|
.join(' AND ');
|
|
@@ -33,10 +32,20 @@ export async function saveFailedAssertCapsule(inputs) {
|
|
|
33
32
|
// verdict is already built, and losing the capsule must never also lose the verdict.
|
|
34
33
|
if (root === undefined)
|
|
35
34
|
return undefined;
|
|
36
|
-
|
|
35
|
+
// Built BEFORE the id, because the id carries the fingerprint of this body — that is what lets a
|
|
36
|
+
// retry of the same broken step fold into the capsule already on disk instead of adding a file.
|
|
37
|
+
/*
|
|
38
|
+
* What this capsule is ABOUT, for its filename and its anchor.
|
|
39
|
+
*
|
|
40
|
+
* The testid is the element said in a name a human chose; `args.ref` is a volatile handle minted
|
|
41
|
+
* per session, meaningless an hour later and anywhere else. The anchor may still fall back to the
|
|
42
|
+
* ref, because the anchor has to address the element and the ref is what addressed it. The NAME
|
|
43
|
+
* must not: a capsule is durable, meant to be committed, and read by somebody who was not there.
|
|
44
|
+
*/
|
|
45
|
+
const testid = asString(asRecord(actResult.result)['testid']);
|
|
46
|
+
const action = asString(args['action']) ?? ActionType.CLICK;
|
|
47
|
+
const body = {
|
|
37
48
|
version: CAPSULE_VERSION,
|
|
38
|
-
id,
|
|
39
|
-
createdAt: deps.now(),
|
|
40
49
|
origin: 'failed-assert',
|
|
41
50
|
expected: expectedText.length > 0 ? expectedText : 'declared consequence',
|
|
42
51
|
observed: capsule.firstDivergence?.observed ?? verdict.failureReason ?? 'not observed',
|
|
@@ -45,14 +54,20 @@ export async function saveFailedAssertCapsule(inputs) {
|
|
|
45
54
|
tool: ReticleTool.ACT,
|
|
46
55
|
anchor: {
|
|
47
56
|
kind: AnchorKind.TESTID,
|
|
48
|
-
value:
|
|
57
|
+
value: testid ?? asString(args['ref']) ?? '',
|
|
49
58
|
// Carried so the saved capsule — which outlives this turn and becomes a regression flow
|
|
50
59
|
// when it goes green — still knows which file the failure came from.
|
|
51
60
|
...(actedSource === undefined ? {} : { source: actedSource }),
|
|
52
61
|
},
|
|
53
|
-
action:
|
|
62
|
+
action: action,
|
|
54
63
|
},
|
|
55
64
|
],
|
|
65
|
+
};
|
|
66
|
+
const id = capsuleId(deps.now(), testid ?? action, capsuleFingerprint(body));
|
|
67
|
+
const saved = await new CapsuleStore(deps.fs, root).save({
|
|
68
|
+
...body,
|
|
69
|
+
id,
|
|
70
|
+
createdAt: deps.now(),
|
|
56
71
|
});
|
|
57
72
|
return saved ? id : undefined;
|
|
58
73
|
}
|
|
@@ -42,7 +42,6 @@ import { waitForReaction } from './act/react-grace.js';
|
|
|
42
42
|
import { decideVerified } from '@reticlehq/engine/evidence/verified.js';
|
|
43
43
|
import { honestyForVerdict } from '@reticlehq/engine/evidence/honesty.js';
|
|
44
44
|
import { declaredExpectations, declaresBodyIndependentChannel, } from '@reticlehq/engine/question/declared.js';
|
|
45
|
-
import { readsDomState, alreadyTrueHiddenMatch as alreadyTrueHiddenMatchOf, } from '@reticlehq/engine/evidence/already-true.js';
|
|
46
45
|
import { describeWaitTarget, namedNetIsInFlight } from '@reticlehq/engine/evidence/unsettled.js';
|
|
47
46
|
import { saveFailedAssertCapsule } from './act-capsule.js';
|
|
48
47
|
import { blastRadius, buildDivergenceCapsule, wireCapsule } from '../../judgement/capsule/capsule.js';
|
|
@@ -52,7 +51,8 @@ import { absenceBlindSpotNote, buildCoverageStatement, blindSpotsFromState, tran
|
|
|
52
51
|
import { acceptedWriteLabels } from '@reticlehq/engine/evidence/accepted-write.js';
|
|
53
52
|
import { unreadWriteLabels } from '@reticlehq/engine/evidence/unread-outcome.js';
|
|
54
53
|
import { evaluatePredicate, waitForPredicate, provenExpectedLinks, PredicateSchema, } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
55
|
-
import {
|
|
54
|
+
import { sessionVerdictFacts } from '../../portal/session/session-verdict-facts.js';
|
|
55
|
+
import { currentOf, healthEnvelope, refuseIfThrottled } from '../../portal/session/session-health.js';
|
|
56
56
|
import { pausedShortCircuit, pausedOutputShape, withControl, PAUSED_NO_VERDICT, } from '../../portal/session/control-envelope.js';
|
|
57
57
|
import { asNumber, asRecord, asString } from '@reticlehq/core';
|
|
58
58
|
import { sourceOf } from './tools-helpers.js';
|
|
@@ -64,6 +64,7 @@ import { asActionType, gradeOf } from './act/act-helpers.js';
|
|
|
64
64
|
import { resolveActTarget } from './act/act-target.js';
|
|
65
65
|
import { tryRealInput, rewriteUploadArgs, HOVER_NEEDS_POINTER_MSG } from './real-input-attempt.js';
|
|
66
66
|
import { gradeOfPredicate } from './assert/assert-grade.js';
|
|
67
|
+
import { readAlreadyTrue } from './act/already-true.js';
|
|
67
68
|
/**
|
|
68
69
|
* Narrow the wire's `action` to a real ActionType, or undefined.
|
|
69
70
|
*
|
|
@@ -428,22 +429,10 @@ export const ACT_TOOLS = [
|
|
|
428
429
|
// A verdict that lives only in the response lives only in the agent's context window, which is
|
|
429
430
|
// exactly the copy a compaction destroys — see runs/run-context.ts.
|
|
430
431
|
let verdictEffect;
|
|
431
|
-
// Was the
|
|
432
|
-
|
|
433
|
-
// past, so they need no pre-check and pay nothing. One extra query, on the path where a green
|
|
434
|
-
// is otherwise unfalsifiable. See engine/src/evidence/already-true.ts.
|
|
435
|
-
const alreadyTruePrecheck = until !== undefined && readsDomState(until)
|
|
436
|
-
? await evaluatePredicate(session, until, since, false)
|
|
437
|
-
: undefined;
|
|
438
|
-
const alreadyTrue = alreadyTruePrecheck?.pass ?? false;
|
|
432
|
+
// Was the consequence ALREADY TRUE, and what did that reading say? See act/already-true.ts.
|
|
433
|
+
const { alreadyTrue, alreadyTrueHiddenMatch, alreadyTrueEvidence, baselines } = await readAlreadyTrue(session, until, since);
|
|
439
434
|
// Same as the ACT handler: the route this step RAN on, before the action can move the app.
|
|
440
435
|
const routeBeforeWait = pathOf(session.url);
|
|
441
|
-
// #889: the pre-check evidence is already in hand — cheap to also ask whether that match was
|
|
442
|
-
// against something hidden, so the already_true message can name it instead of leaving the
|
|
443
|
-
// agent to re-derive "was this actually showing?" from nothing.
|
|
444
|
-
const alreadyTrueHiddenMatch = alreadyTrue && until !== undefined
|
|
445
|
-
? alreadyTrueHiddenMatchOf(until, alreadyTruePrecheck?.evidence)
|
|
446
|
-
: false;
|
|
447
436
|
try {
|
|
448
437
|
// actCommand is the single interception point for upload+path rewrite.
|
|
449
438
|
//
|
|
@@ -472,8 +461,8 @@ export const ACT_TOOLS = [
|
|
|
472
461
|
let verdict = null === actResult
|
|
473
462
|
? { pass: false, observationLost: true }
|
|
474
463
|
: timeout > 0
|
|
475
|
-
? await waitForPredicate(session, until, timeout, since)
|
|
476
|
-
: await evaluatePredicate(session, until, since);
|
|
464
|
+
? await waitForPredicate(session, until, timeout, since, baselines)
|
|
465
|
+
: await evaluatePredicate(session, until, since, true, baselines);
|
|
477
466
|
// The SDK may have gone away mid-act — see act-observation.ts.
|
|
478
467
|
const followed = await followLostObservation({
|
|
479
468
|
sessions: deps.sessions,
|
|
@@ -481,7 +470,7 @@ export const ACT_TOOLS = [
|
|
|
481
470
|
verdict,
|
|
482
471
|
timeout,
|
|
483
472
|
predicateStarted,
|
|
484
|
-
reevaluate: (next, budget) => waitForPredicate(next, until, budget, 0),
|
|
473
|
+
reevaluate: (next, budget) => waitForPredicate(next, until, budget, 0, baselines),
|
|
485
474
|
});
|
|
486
475
|
if (followed.followed)
|
|
487
476
|
since = 0;
|
|
@@ -654,17 +643,17 @@ export const ACT_TOOLS = [
|
|
|
654
643
|
currentDocumentId: session.currentDocumentId,
|
|
655
644
|
currentEditEpoch: session.currentEditEpoch,
|
|
656
645
|
appOrigin: session.url,
|
|
646
|
+
background: session.background,
|
|
657
647
|
});
|
|
658
648
|
// The single field an agent reads. Everything below it is the evidence it was derived from;
|
|
659
649
|
// this is the only one that has to be interpreted, and now it interprets itself.
|
|
660
650
|
const outcomePending = acceptedWriteLabels(windowEvents);
|
|
661
651
|
const outcomeUnread = unreadWriteLabels(windowEvents);
|
|
662
|
-
const stillInFlight = inFlightRequestLabels(windowEvents);
|
|
652
|
+
const stillInFlight = inFlightRequestLabels(windowEvents, session.url, session.background);
|
|
663
653
|
const decision = decideVerified({
|
|
664
654
|
pass: verdict.pass,
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
...(session.sdkVersion === undefined ? {} : { sdkVersion: session.sdkVersion }),
|
|
655
|
+
// Threaded rather than looked up: decideVerified is pure and has no session.
|
|
656
|
+
...sessionVerdictFacts(session),
|
|
668
657
|
// The caller NAMED the consequence rather than defaulting to "wait for idle". A
|
|
669
658
|
// declaration made before the action is what this tool sells, and idle-settlement was
|
|
670
659
|
// overriding it — see `declaredConsequence`. An explicit `{ kind: "settled" }` is not a
|
|
@@ -770,6 +759,16 @@ export const ACT_TOOLS = [
|
|
|
770
759
|
// which is why the two are recorded apart rather than assumed alike.
|
|
771
760
|
declaredBeforeActing: true,
|
|
772
761
|
grade: gradeOfPredicate(until),
|
|
762
|
+
// What the declared consequence READ. The run fold has only the journal, so a kind absent
|
|
763
|
+
// here was being invented there as `element`. Same field, same reason, on the assert path.
|
|
764
|
+
kind: until.kind,
|
|
765
|
+
// The deciding clause, kept — and it was NOT, on this path, the commonest one. `unknown`
|
|
766
|
+
// alone cannot tell an outcome that has not arrived from a capture that could not be
|
|
767
|
+
// read, so a fold over the journal saw a verdict it could not attribute, and the
|
|
768
|
+
// attribution is the only part of an `unknown` a reader can act on. The comment above
|
|
769
|
+
// says "same field, same reason, on the assert path" and was half true. Found by the
|
|
770
|
+
// session gap summary, whose `undecidedBy` was empty for every unknown this tool made.
|
|
771
|
+
reason: decision.verifiedReason,
|
|
773
772
|
};
|
|
774
773
|
// Recorded on the session, so a later "am I done?" can answer with what is STILL missing
|
|
775
774
|
// rather than with everything that was ever missing. An empty list closes a gap, which is
|
|
@@ -777,6 +776,8 @@ export const ACT_TOOLS = [
|
|
|
777
776
|
noteSessionGaps(session, gaps);
|
|
778
777
|
return withControl(session, {
|
|
779
778
|
...decision,
|
|
779
|
+
// WHAT was already true, not only that something was (4.1) — see act/already-true.ts.
|
|
780
|
+
...(alreadyTrueEvidence === undefined ? {} : { alreadyTrueEvidence }),
|
|
780
781
|
// An unobserved act has no effect to report, and inventing an empty one would read as
|
|
781
782
|
// "the page did nothing" — a claim about the app, from a call that never saw it.
|
|
782
783
|
...(null === actResult ? {} : { effect: leanActResult(actResult.result) }),
|
|
@@ -823,7 +824,7 @@ export const ACT_TOOLS = [
|
|
|
823
824
|
...(0 === greenRadius.length ? {} : { blastRadius: greenRadius }),
|
|
824
825
|
since,
|
|
825
826
|
...(session.id === actedSessionId ? {} : { sessionId: session.id }),
|
|
826
|
-
...healthEnvelope(session),
|
|
827
|
+
...healthEnvelope(currentOf(deps.sessions, session)),
|
|
827
828
|
});
|
|
828
829
|
}
|
|
829
830
|
finally {
|
|
@@ -31,8 +31,6 @@ export declare const MAX_BLOCKING_WAIT_MS: number;
|
|
|
31
31
|
* in the name of bounding one that never worked.
|
|
32
32
|
*/
|
|
33
33
|
export declare const MAX_STATE_DEPTH = 256;
|
|
34
|
-
/** Scroll-to-find steps. Default is 20; hundreds is searching, thousands is a hang. */
|
|
35
|
-
export declare const MAX_SCROLLS = 200;
|
|
36
34
|
/** Viewport CSS px — matches the clamp the handler already applied, now as a refusal. */
|
|
37
35
|
export declare const MIN_VIEWPORT_PX = 64;
|
|
38
36
|
export declare const MAX_VIEWPORT_PX = 10000;
|
|
@@ -45,6 +43,21 @@ export declare const cursorSchema: z.ZodNumber;
|
|
|
45
43
|
* A wait budget in ms. 0 means evaluate now (documented on assert). Negative cannot be honoured.
|
|
46
44
|
*/
|
|
47
45
|
export declare const timeoutMsSchema: z.ZodNumber;
|
|
46
|
+
/**
|
|
47
|
+
* `reticle_wait_for`'s budget, which may exceed one call: it waits at most MAX_BLOCKING_WAIT_MS per
|
|
48
|
+
* call and hands back `resume_ms` for the rest (#601, from #635).
|
|
49
|
+
*/
|
|
50
|
+
export declare const waitForTimeoutMsSchema: z.ZodNumber;
|
|
51
|
+
/**
|
|
52
|
+
* What is left of a wait that stopped at the per-call limit, or undefined when there is nothing to
|
|
53
|
+
* resume: the predicate held, the whole budget fit in this call, or the wait ended for a reason
|
|
54
|
+
* waiting longer cannot change (the page went away, or the read could not be made).
|
|
55
|
+
*/
|
|
56
|
+
export declare function resumeAfter(requestedMs: number, perCallMs: number, verdict: {
|
|
57
|
+
pass: boolean;
|
|
58
|
+
observationLost?: boolean;
|
|
59
|
+
inconclusive?: string;
|
|
60
|
+
}): number | undefined;
|
|
48
61
|
/**
|
|
49
62
|
* A count / cap. 0 means "return none", which is a real request. Negative is not.
|
|
50
63
|
* Capped at the ring-buffer size so a `limit: 1e9` cannot pretend the buffer is larger than it is.
|
|
@@ -38,8 +38,8 @@ export const MAX_TIMEOUT_MS = 120_000;
|
|
|
38
38
|
*
|
|
39
39
|
* This does not make long waits possible, and is not meant to — it makes the ADVERTISED bound one
|
|
40
40
|
* that can actually be honoured. A caller that genuinely needs to outlast this polls: several short
|
|
41
|
-
* waits, each of which returns a verdict.
|
|
42
|
-
*
|
|
41
|
+
* waits, each of which returns a verdict. `reticle_wait_for` does that for the caller: see
|
|
42
|
+
* `waitForTimeoutMsSchema` and `resumeAfter`.
|
|
43
43
|
*/
|
|
44
44
|
const MCP_SDK_DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
45
45
|
export const MAX_BLOCKING_WAIT_MS = MCP_SDK_DEFAULT_REQUEST_TIMEOUT_MS - 5_000;
|
|
@@ -54,7 +54,7 @@ export const MAX_STATE_DEPTH = 256;
|
|
|
54
54
|
/** A crawl that clicks forever is not a bound. */
|
|
55
55
|
const MAX_CRAWL_STEPS = 500;
|
|
56
56
|
/** Scroll-to-find steps. Default is 20; hundreds is searching, thousands is a hang. */
|
|
57
|
-
|
|
57
|
+
const MAX_SCROLLS = 200;
|
|
58
58
|
/** Fake-clock jump. One day of timers is a test; a billion ms is not. */
|
|
59
59
|
const MAX_ADVANCE_MS = 86_400_000;
|
|
60
60
|
/** Presenter idle window. One hour is a slow app; larger values never fire. */
|
|
@@ -71,6 +71,33 @@ export const cursorSchema = z.number().finite().int().nonnegative();
|
|
|
71
71
|
* A wait budget in ms. 0 means evaluate now (documented on assert). Negative cannot be honoured.
|
|
72
72
|
*/
|
|
73
73
|
export const timeoutMsSchema = z.number().finite().int().nonnegative().max(MAX_BLOCKING_WAIT_MS);
|
|
74
|
+
/**
|
|
75
|
+
* The longest `reticle_wait_for` accepts, across as many calls as it takes. Ten minutes is a slow
|
|
76
|
+
* build or a queued job; an hour is a hang somebody forgot about.
|
|
77
|
+
*/
|
|
78
|
+
const MAX_RESUMABLE_WAIT_MS = 600_000;
|
|
79
|
+
/**
|
|
80
|
+
* `reticle_wait_for`'s budget, which may exceed one call: it waits at most MAX_BLOCKING_WAIT_MS per
|
|
81
|
+
* call and hands back `resume_ms` for the rest (#601, from #635).
|
|
82
|
+
*/
|
|
83
|
+
export const waitForTimeoutMsSchema = z
|
|
84
|
+
.number()
|
|
85
|
+
.finite()
|
|
86
|
+
.int()
|
|
87
|
+
.nonnegative()
|
|
88
|
+
.max(MAX_RESUMABLE_WAIT_MS);
|
|
89
|
+
/**
|
|
90
|
+
* What is left of a wait that stopped at the per-call limit, or undefined when there is nothing to
|
|
91
|
+
* resume: the predicate held, the whole budget fit in this call, or the wait ended for a reason
|
|
92
|
+
* waiting longer cannot change (the page went away, or the read could not be made).
|
|
93
|
+
*/
|
|
94
|
+
export function resumeAfter(requestedMs, perCallMs, verdict) {
|
|
95
|
+
if (verdict.pass || true === verdict.observationLost || verdict.inconclusive !== undefined) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const left = requestedMs - perCallMs;
|
|
99
|
+
return left > 0 ? left : undefined;
|
|
100
|
+
}
|
|
74
101
|
/**
|
|
75
102
|
* A count / cap. 0 means "return none", which is a real request. Negative is not.
|
|
76
103
|
* Capped at the ring-buffer size so a `limit: 1e9` cannot pretend the buffer is larger than it is.
|
|
@@ -115,11 +115,23 @@ export function gradeOfPredicate(predicate) {
|
|
|
115
115
|
return HonestyGrade.PRESENCE;
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Fold a combinator's branches on the ladder. No branches proves nothing, so it stays at the floor.
|
|
120
|
+
*
|
|
121
|
+
* The floor is `NONE`, not `PRESENCE`. This sentence has been here since the function was written
|
|
122
|
+
* and the code returned one rung above it, which is the entire defect: `VACUOUS_GRADE`
|
|
123
|
+
* (`engine/src/evidence/verified.ts`) fires on `NONE`, so an empty combinator graded `PRESENCE`
|
|
124
|
+
* walked past the one guard whose job is refusing a green that rests on nothing — and reported
|
|
125
|
+
* `verified: "yes"` for a drive that asserted nothing at all.
|
|
126
|
+
*
|
|
127
|
+
* The schema refuses an empty combinator now, so this is the second defence rather than the only
|
|
128
|
+
* one. It is kept because the two stop different things: the schema stops it ENTERING through the
|
|
129
|
+
* documented door, and this stops it COUNTING if it is ever constructed rather than parsed.
|
|
130
|
+
*/
|
|
119
131
|
function combine(branches, pick) {
|
|
120
132
|
const grades = branches.map(gradeOfPredicate);
|
|
121
133
|
const first = grades[0];
|
|
122
134
|
if (first === undefined)
|
|
123
|
-
return HonestyGrade.
|
|
135
|
+
return HonestyGrade.NONE;
|
|
124
136
|
return grades.reduce(pick, first);
|
|
125
137
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CaptureLoss, channelsRead, PredicateKind } from '@reticlehq/core';
|
|
2
|
+
import { sessionVerdictFacts } from '../../../portal/session/session-verdict-facts.js';
|
|
2
3
|
import { gapsForAction } from '@reticlehq/engine/evidence/instrumentation-gaps.js';
|
|
3
4
|
import { noteSessionGaps } from '@reticlehq/engine/evidence/gap-ledger.js';
|
|
4
5
|
import { declaresState } from '@reticlehq/engine/question/predicate/predicate-asks.js';
|
|
@@ -15,6 +16,7 @@ import { describeWaitTarget, namedNetIsInFlight } from '@reticlehq/engine/eviden
|
|
|
15
16
|
import { inFlightRequestLabels, repeatedRequestLabels, } from '../act/settle-in-flight.js';
|
|
16
17
|
import { gradeOfPredicate } from './assert-grade.js';
|
|
17
18
|
import { assertSource } from './assert-source.js';
|
|
19
|
+
import { VerdictAttribution, verdictAttributionOf } from '@reticlehq/core';
|
|
18
20
|
/**
|
|
19
21
|
* The honesty verdict for a plain `reticle_assert`.
|
|
20
22
|
*
|
|
@@ -85,6 +87,7 @@ changeUndeclared) {
|
|
|
85
87
|
currentDocumentId: session.currentDocumentId,
|
|
86
88
|
currentEditEpoch: session.currentEditEpoch,
|
|
87
89
|
appOrigin: session.url,
|
|
90
|
+
background: session.background,
|
|
88
91
|
expectedFailures: declared.netFailures,
|
|
89
92
|
namedNetUrls: declared.netUrls,
|
|
90
93
|
renderProved: pass && declared.rendersContent,
|
|
@@ -114,7 +117,7 @@ changeUndeclared) {
|
|
|
114
117
|
const impeachingNotes = [impeaching.note, gap, ...crashedRuleNotes()].filter((n) => n !== undefined);
|
|
115
118
|
const outcomePending = acceptedWriteLabels(windowEvents);
|
|
116
119
|
const outcomeUnread = unreadWriteLabels(windowEvents);
|
|
117
|
-
const stillInFlight = inFlightRequestLabels(windowEvents);
|
|
120
|
+
const stillInFlight = inFlightRequestLabels(windowEvents, session.url, session.background);
|
|
118
121
|
const effectiveInconclusive = inconclusive ?? (!pass ? session.preconditionFailure?.() : undefined);
|
|
119
122
|
/**
|
|
120
123
|
* Does a green here rest on the window having been COMPLETE?
|
|
@@ -147,9 +150,8 @@ changeUndeclared) {
|
|
|
147
150
|
// treating that silence as an empty set would refuse every claim from every older page.
|
|
148
151
|
channelsRead: channelsRead(predicate.kind),
|
|
149
152
|
...(session.channels === undefined ? {} : { channelsObservable: session.channels }),
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
...(session.sdkVersion === undefined ? {} : { sdkVersion: session.sdkVersion }),
|
|
153
|
+
// Threaded rather than looked up: decideVerified is pure and has no session.
|
|
154
|
+
...sessionVerdictFacts(session),
|
|
153
155
|
// Same rule as the act path: the caller named a consequence, so a settlement-only finding must
|
|
154
156
|
// not override it. A fix that lived on one half of the verdict surface would leave the other
|
|
155
157
|
// half broken, and this is the tool agents call most.
|
|
@@ -217,6 +219,8 @@ changeUndeclared) {
|
|
|
217
219
|
pass,
|
|
218
220
|
lastActSource: session.lastAct.source(),
|
|
219
221
|
});
|
|
222
|
+
// Who can act on this verdict, derived from the clause that decided it — see `attributedTo` below.
|
|
223
|
+
const attributedTo = verdictAttributionOf(decision.verifiedReason);
|
|
220
224
|
return {
|
|
221
225
|
decision: decision,
|
|
222
226
|
contradictions,
|
|
@@ -226,6 +230,9 @@ changeUndeclared) {
|
|
|
226
230
|
claim: describeWaitTarget(predicate),
|
|
227
231
|
verified: decision.verified,
|
|
228
232
|
...(source === undefined ? {} : { source }),
|
|
233
|
+
// What answering this claim READ. Known here and nowhere later: a run folded from the journal
|
|
234
|
+
// has only what the journal kept, and without this it stamped `element` on every check.
|
|
235
|
+
kind: predicate.kind,
|
|
229
236
|
// The three facts that make a verdict something other than a pass/fail line. All three were
|
|
230
237
|
// known right here and none of them survived into the durable record, so nothing downstream
|
|
231
238
|
// could report them however well this moment understood them.
|
|
@@ -240,6 +247,25 @@ changeUndeclared) {
|
|
|
240
247
|
// arrived from a capture that could not be read, and only the first is worth asking about
|
|
241
248
|
// again -- which is what makes a later answer a CORRECTION rather than a second opinion.
|
|
242
249
|
reason: decision.verifiedReason,
|
|
250
|
+
/*
|
|
251
|
+
* WHOSE problem it is, in four values, finishing the sentence above.
|
|
252
|
+
*
|
|
253
|
+
* The reason names the deciding clause; this names who can act on it, which for an `unknown`
|
|
254
|
+
* is the only question the reader has. `environment` resolves by waiting, `could-not-see` by
|
|
255
|
+
* looking again with better coverage, `harness` by fixing Reticle or the call, and `code` is
|
|
256
|
+
* the only one that means go and change the app. Omitted on a proof, which has no owner.
|
|
257
|
+
*
|
|
258
|
+
* Derived here rather than inside the verdict: it is a pure function of a reason the engine
|
|
259
|
+
* already returns, and `core-coupling-only-shrinks` caps what the engine may borrow from core.
|
|
260
|
+
*
|
|
261
|
+
* Named `attributedTo`, not `attribution`, because two other things already carry that word:
|
|
262
|
+
* `honesty.attribution` a few lines up ("what the grade is attributed to" — the window), and
|
|
263
|
+
* `BugAttribution` in telemetry ("we found a defect, whose defect is it?"). Three meanings on
|
|
264
|
+
* one noun is how a reader ends up acting on the wrong one.
|
|
265
|
+
*/
|
|
266
|
+
...(attributedTo === undefined || VerdictAttribution.NONE === attributedTo
|
|
267
|
+
? {}
|
|
268
|
+
: { attributedTo }),
|
|
243
269
|
grade: gradeOfPredicate(predicate),
|
|
244
270
|
couldNotSee: spots.map((spot) => spot.kind),
|
|
245
271
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { carryReticleIdentity } from './lease-tools.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { navigateResult } from './act/navigation/navigate-result.js';
|
|
4
|
-
import { awaitArrival, idsAtTarget, ARRIVAL_TIMEOUT_MS, } from './act/navigation/navigate-arrival.js';
|
|
4
|
+
import { absoluteTarget, awaitArrival, idsAtTarget, ARRIVAL_TIMEOUT_MS, } from './act/navigation/navigate-arrival.js';
|
|
5
5
|
import { reloadResult } from './act/navigation/reload-result.js';
|
|
6
6
|
import { waitForReconnect, RELOAD_RECONNECT_TIMEOUT_MS, } from '../../portal/session/session-reconnect.js';
|
|
7
7
|
import { ReticleCommand } from '@reticlehq/core';
|
|
@@ -89,7 +89,8 @@ export const BROWSER_TOOLS = [
|
|
|
89
89
|
// Sampled BEFORE dispatch, because that is the only moment it can be known. A session already
|
|
90
90
|
// sitting on the target is not evidence that THIS navigation arrived — and the arrival scan,
|
|
91
91
|
// which reads whatever is at the target afterwards, cannot tell the two apart on its own.
|
|
92
|
-
const
|
|
92
|
+
const target = absoluteTarget(url, session.url);
|
|
93
|
+
const priorIds = idsAtTarget(deps.sessions, target);
|
|
93
94
|
session.beginAction(ReticleTool.NAVIGATE, { url });
|
|
94
95
|
// Same floor as the reload path above, for the same reason: going to a new URL replaces the
|
|
95
96
|
// document just as thoroughly. `beginAction` attributes events to this action; it does not move
|
|
@@ -103,7 +104,7 @@ export const BROWSER_TOOLS = [
|
|
|
103
104
|
// reticle_sessions itself. A refusal skips the wait: there is nothing to wait FOR.
|
|
104
105
|
const timeoutMs = asNumber(args['timeout_ms']) ?? ARRIVAL_TIMEOUT_MS;
|
|
105
106
|
const arrival = true === result.ok
|
|
106
|
-
? await awaitArrival(deps.sessions,
|
|
107
|
+
? await awaitArrival(deps.sessions, target, { navigatedId: session.id, priorIds }, timeoutMs)
|
|
107
108
|
: null;
|
|
108
109
|
return navigateResult(result, arrival, timeoutMs);
|
|
109
110
|
}
|
|
@@ -25,6 +25,8 @@ function paramInfo(shape) {
|
|
|
25
25
|
function unknownParamsError(toolName, unknown) {
|
|
26
26
|
return `unknown ${1 === unknown.length ? 'parameter' : 'parameters'} for ${toolName}: ${unknown.join(', ')} — NOT applied, so any result would be an answer to a different question`;
|
|
27
27
|
}
|
|
28
|
+
/** The one `action` reticle_tools takes: every other tool on the surface is action-dispatched. */
|
|
29
|
+
const TOOLS_LIST_ACTION = 'list';
|
|
28
30
|
/** What to write instead — the one parameter reticle_tools declares, and the no-argument form. */
|
|
29
31
|
const TOOLS_ARG_HINT = 'name the tools you want in `names`: reticle_tools { names: ["reticle_act_and_wait"] } — or call it with no arguments for the full catalog';
|
|
30
32
|
/** The keys not declared by `shape`, in call order. */
|
|
@@ -92,6 +94,10 @@ callable) {
|
|
|
92
94
|
},
|
|
93
95
|
};
|
|
94
96
|
const toolsShape = {
|
|
97
|
+
action: z
|
|
98
|
+
.literal(TOOLS_LIST_ACTION)
|
|
99
|
+
.optional()
|
|
100
|
+
.describe('"list": the catalogue, same as no arguments.'),
|
|
95
101
|
names: z
|
|
96
102
|
.array(z.string())
|
|
97
103
|
.optional()
|
|
@@ -8,7 +8,7 @@ export declare const FEEDBACK_TOOLS: ToolDef[];
|
|
|
8
8
|
* Delivered ONCE per server process. A prompt on every verification would be nagging, and a nagged
|
|
9
9
|
* user opts out of telemetry entirely, which costs us the counters too.
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
declare const FEEDBACK_PROMPT: {
|
|
12
12
|
readonly ask: "Ask the human, in your own words and only once: how did this verification go, and what would have made Reticle more useful? If they answer, file it with `reticle feedback` (they run it) or relay it yourself.";
|
|
13
13
|
readonly command: "reticle feedback";
|
|
14
14
|
};
|
|
@@ -33,3 +33,4 @@ export declare const VERDICT_TOOLS: ReadonlySet<string>;
|
|
|
33
33
|
*/
|
|
34
34
|
export declare function takeFeedbackPrompt(toolName: string): typeof FEEDBACK_PROMPT | undefined;
|
|
35
35
|
export declare function resetFeedbackPrompt(): void;
|
|
36
|
+
export {};
|
|
@@ -196,7 +196,7 @@ export const FEEDBACK_TOOLS = [
|
|
|
196
196
|
* Delivered ONCE per server process. A prompt on every verification would be nagging, and a nagged
|
|
197
197
|
* user opts out of telemetry entirely, which costs us the counters too.
|
|
198
198
|
*/
|
|
199
|
-
|
|
199
|
+
const FEEDBACK_PROMPT = {
|
|
200
200
|
ask: 'Ask the human, in your own words and only once: how did this verification go, and what would have made Reticle more useful? If they answer, file it with `reticle feedback` (they run it) or relay it yourself.',
|
|
201
201
|
command: 'reticle feedback',
|
|
202
202
|
};
|
|
@@ -17,7 +17,7 @@ import { reportOnboardingStep } from '../../telemetry/onboarding-funnel.js';
|
|
|
17
17
|
import { noteActed, noteFirstVerdict, noteOnboardingFirst } from '../../telemetry/onboarding-firsts.js';
|
|
18
18
|
import { withHarnessDrive } from '../../telemetry/harness-drive.js';
|
|
19
19
|
import { OnboardingPhase, OnboardingStepStatus } from '@reticlehq/core/telemetry';
|
|
20
|
-
import { asString } from '@reticlehq/core';
|
|
20
|
+
import { DiscoveryInvite, asString } from '@reticlehq/core';
|
|
21
21
|
import { SESSION_ID_ARG, sessionIdFromArgs, spentRefFromArgs } from './tools-helpers.js';
|
|
22
22
|
import { EnvelopeKey } from './tool-kit.js';
|
|
23
23
|
import { ReticleTool } from '@reticlehq/core';
|
|
@@ -134,7 +134,7 @@ export const SESSION_EXEMPT_TOOLS = new Set([
|
|
|
134
134
|
// FLOW_HEAL is no longer a name runTool ever sees: it became `reticle_verify {action:"heal"}`,
|
|
135
135
|
// and VERIFY is already listed here, so the exemption is inherited from the parent. Left as a
|
|
136
136
|
// dangling name it would be a lie this file's own test 6 is written to catch.
|
|
137
|
-
ReticleTool.INTENT, // reads/writes .reticle/intent
|
|
137
|
+
ReticleTool.INTENT, // reads/writes .reticle/intent/; sessionId only picks the project
|
|
138
138
|
ReticleTool.CONTEXT, // folds the journal + intent ledger; must still answer when nothing is connected
|
|
139
139
|
ReticleTool.PROJECT, // reads .reticle/project.json
|
|
140
140
|
// Reads the team's shared memory over HTTP; sessionId only resolves WHICH project's link file to
|
|
@@ -609,7 +609,13 @@ export async function runTool(tool, deps, args) {
|
|
|
609
609
|
? raw
|
|
610
610
|
: {
|
|
611
611
|
...raw,
|
|
612
|
-
...(friction !== undefined
|
|
612
|
+
...(friction !== undefined
|
|
613
|
+
? {
|
|
614
|
+
[EnvelopeKey.FEEDBACK_INVITE]: inviteFor(friction),
|
|
615
|
+
// The moment Reticle got in somebody's way is the moment to offer them the founder.
|
|
616
|
+
[EnvelopeKey.TALK_TO_US]: DiscoveryInvite.AGENT,
|
|
617
|
+
}
|
|
618
|
+
: {}),
|
|
613
619
|
...(prompt !== undefined ? { [EnvelopeKey.FEEDBACK_PROMPT]: prompt } : {}),
|
|
614
620
|
...(update !== undefined ? { [EnvelopeKey.UPDATE_AVAILABLE]: update } : {}),
|
|
615
621
|
...(skew !== undefined ? { [EnvelopeKey.VERSION_SKEW]: skew } : {}),
|
|
@@ -623,7 +629,14 @@ export async function runTool(tool, deps, args) {
|
|
|
623
629
|
return result;
|
|
624
630
|
// Reuse the session resolved above so the health envelope describes the SAME session the handler
|
|
625
631
|
// drove; only re-resolve if the up-front attempt failed but the handler somehow succeeded.
|
|
626
|
-
const
|
|
632
|
+
const driven = session ?? deps.sessions.resolve(rawSessionId);
|
|
633
|
+
// ...unless the call replaced that document. A full navigation or reload registers a NEW Session
|
|
634
|
+
// under the same id (or under the id the result reports it arrived at), and the object resolved
|
|
635
|
+
// before the call is the page that unloaded: it reported itself hidden on the way out, so every
|
|
636
|
+
// response after a navigation said "throttled" about a tab that was fine. Describe what is there
|
|
637
|
+
// now. Optional-called because a test double is a partial SessionManager.
|
|
638
|
+
const arrivedId = 'string' === typeof result['sessionId'] ? result['sessionId'] : driven.id;
|
|
639
|
+
const resolved = deps.sessions.get?.(arrivedId) ?? driven;
|
|
627
640
|
const envelope = {};
|
|
628
641
|
// The health block is idempotent: add it only when the handler didn't already include a `session`.
|
|
629
642
|
if (!('session' in result))
|
|
@@ -36,6 +36,14 @@ export interface LeaseEvidence {
|
|
|
36
36
|
* reported as a marker that was not found.
|
|
37
37
|
*/
|
|
38
38
|
sdkMarker?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The page's own Content-Security-Policy, read off the served document, when it refuses the
|
|
41
|
+
* bridge. Proof rather than a guess: the connection cannot happen until the policy allows it.
|
|
42
|
+
*/
|
|
43
|
+
cspBlock?: {
|
|
44
|
+
problem: string;
|
|
45
|
+
fix: string;
|
|
46
|
+
};
|
|
39
47
|
/**
|
|
40
48
|
* The websocket URL the leased page actually tried, read from its own console.
|
|
41
49
|
*
|
|
@@ -85,6 +85,16 @@ export function leaseNotConnectedHint(url, port, evidence = {}) {
|
|
|
85
85
|
`RETICLE_PORT, or the build plugin's \`port\`), and \`VITE_RETICLE_WS_URL\` overrides it ` +
|
|
86
86
|
`outright.${marker} ${RELEASE}`);
|
|
87
87
|
}
|
|
88
|
+
// 0b. The page's own policy forbids the connection. Read off the document it served, so it is
|
|
89
|
+
// proof about the page in hand, and it holds whether or not the app ships an SDK: neither
|
|
90
|
+
// `init` nor the zero-install reader can dial out of a page that forbids it. Every cause
|
|
91
|
+
// below presupposes a page that is allowed to connect, which is why this comes first.
|
|
92
|
+
if (evidence.cspBlock !== undefined) {
|
|
93
|
+
return (`${opening} This page's Content-Security-Policy blocks the connection: ` +
|
|
94
|
+
`${evidence.cspBlock.problem}. That is true whether or not the app has Reticle installed, ` +
|
|
95
|
+
`so installing it will not help until the policy allows the bridge. For development, ` +
|
|
96
|
+
`${evidence.cspBlock.fix}. ${RELEASE}`);
|
|
97
|
+
}
|
|
88
98
|
// 1. A recorded refusal. The only cause with proof behind it, so nothing outranks it.
|
|
89
99
|
if (evidence.refusal !== undefined) {
|
|
90
100
|
return `${opening} A dial DID arrive and was refused: ${evidence.refusal}${marker} ${RELEASE}`;
|