@reticlehq/server 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +16 -13
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/cli-doctor.js +25 -5
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +7 -1
- package/dist/command/cli/cli-parse.js +33 -5
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +129 -17
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +1 -1
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/launch/cli-launch.d.ts +1 -29
- package/dist/command/cli/launch/cli-launch.js +6 -40
- package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
- package/dist/command/cli/ports/resolve/cli-port.js +45 -1
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli.js +5 -0
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +1 -0
- package/dist/features/harness/jev-driver.js +6 -9
- package/dist/features/harness/platform-config.d.ts +10 -0
- package/dist/features/harness/platform-config.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +40 -95
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +31 -9
- package/dist/language/flows/flow-replay-run.js +146 -33
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +12 -11
- package/dist/language/flows/flow-replay.js +79 -109
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flows.d.ts +8 -4
- package/dist/language/flows/flows.js +11 -2
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +12 -8
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/verify-change-tools.js +14 -0
- package/dist/memory/cloud/cloud-sync.d.ts +0 -2
- package/dist/memory/cloud/cloud-sync.js +1 -1
- package/dist/memory/cloud/harness-config.d.ts +2 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +13 -0
- package/dist/memory/cloud/sync-daemon.js +21 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-store.js +6 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +252 -140
- package/dist/memory/intent/intent-shard.d.ts +10 -11
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-recorder.d.ts +21 -0
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +14 -10
- package/dist/memory/journal/on-disk/retention.js +13 -12
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/session-end.d.ts +4 -1
- package/dist/memory/journal/session-end.js +64 -6
- package/dist/memory/journal/session-journal.d.ts +8 -1
- package/dist/memory/journal/session-journal.js +10 -0
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
- package/dist/memory/project/fs/fs-port.js +44 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.js +5 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +9 -0
- package/dist/portal/session/session-manager.js +23 -2
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +19 -7
- package/dist/surface/mcp/mcp-proxy.js +3 -3
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +22 -7
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +30 -4
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/dynamic-tools.js +6 -0
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +17 -4
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/observe-tools.js +14 -6
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +5 -0
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools.js +64 -29
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/cli/cloud.mdx +9 -7
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/desktop.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +128 -4
- package/docs/tools/query.mdx +10 -10
- package/docs/usage.md +1 -1
- package/docs/what-is-recorded.md +62 -0
- package/package.json +6 -6
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -15,11 +15,22 @@
|
|
|
15
15
|
* The clock is injected, per the repo rule: this is arithmetic over timestamps, and a test that
|
|
16
16
|
* needs to wait four seconds to check a four-second gap is a test nobody runs.
|
|
17
17
|
*/
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* What an agent needs to know about the continuity of one session.
|
|
20
|
+
*
|
|
21
|
+
* Every field here measures the SDK-to-daemon LINK. None of them measures the app: the origin the
|
|
22
|
+
* page was served from can stop answering entirely without any of these moving, because the socket
|
|
23
|
+
* that reports them is between the daemon and a document the browser is already holding. Reported
|
|
24
|
+
* as a false read of app health in #938, where a tab whose dev server was dead carried `outages: 0`
|
|
25
|
+
* and `connectedSinceMs: 11min` and was taken for a healthy app.
|
|
26
|
+
*/
|
|
19
27
|
interface Attachment {
|
|
20
28
|
/** How long THIS attachment has lasted. Resets on reconnect — it is not the session's lifetime. */
|
|
21
29
|
connectedSinceMs: number;
|
|
22
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* How many times this session dropped and came back. Zero means the LINK never dropped; it does
|
|
32
|
+
* not mean the app behind it is alive.
|
|
33
|
+
*/
|
|
23
34
|
outages: number;
|
|
24
35
|
/** The most recent gap, when there has been one. */
|
|
25
36
|
lastOutage?: {
|
|
@@ -38,6 +38,22 @@ export interface SessionHealth {
|
|
|
38
38
|
* on `reticle_sessions` or `reticle_lease` is not on the surface it reads every call.
|
|
39
39
|
*/
|
|
40
40
|
versionSkew?: string;
|
|
41
|
+
/**
|
|
42
|
+
* The session's event ledger, once it has grown enough to be worth a reader's attention.
|
|
43
|
+
*
|
|
44
|
+
* Present only past `LEDGER_NOTICE_FRACTION` of the cap, so a healthy session costs nothing --
|
|
45
|
+
* the same rule `pendingNavigationMs` follows. A ledger below that is doing its job quietly.
|
|
46
|
+
*
|
|
47
|
+
* It is here because nothing reported it. A repeating uncaught error wrote one session's ledger
|
|
48
|
+
* until the disk was full; neither the daemon nor `doctor` nor any
|
|
49
|
+
* health counter said a word, and the user found out when the disk was full (#986). A cap bounds
|
|
50
|
+
* the damage but does not make the writing visible, and this block is the surface an agent
|
|
51
|
+
* already reads on every act and assert.
|
|
52
|
+
*/
|
|
53
|
+
ledger?: {
|
|
54
|
+
bytes: number;
|
|
55
|
+
capBytes: number;
|
|
56
|
+
};
|
|
41
57
|
}
|
|
42
58
|
/**
|
|
43
59
|
* How long a request may be in flight before it is worth reporting on session health.
|
|
@@ -84,6 +100,18 @@ interface BufferEnvelope {
|
|
|
84
100
|
* so a clean/empty result there is trustworthy and costs zero tokens.
|
|
85
101
|
*/
|
|
86
102
|
export declare function bufferEnvelope(session: HealthSubject): BufferEnvelope;
|
|
103
|
+
/**
|
|
104
|
+
* The session registered under `session.id` NOW, for describing a tab after a call that may have
|
|
105
|
+
* replaced its document. A reload or full navigation registers a new Session under the same id, and
|
|
106
|
+
* the object a handler resolved first is the page that unloaded; it reported itself hidden on the way
|
|
107
|
+
* out, so its health said "throttled" about a tab that was fine. Optional-called because test doubles
|
|
108
|
+
* are partial registries.
|
|
109
|
+
*/
|
|
110
|
+
export declare function currentOf<S extends {
|
|
111
|
+
id: string;
|
|
112
|
+
}>(registry: {
|
|
113
|
+
get?: (id: string) => S | undefined;
|
|
114
|
+
}, session: S): S;
|
|
87
115
|
/** The `session` (and optional throttled `warning`) block spliced onto act/assert results. */
|
|
88
116
|
interface HealthEnvelope {
|
|
89
117
|
session?: SessionHealth;
|
|
@@ -142,4 +170,37 @@ interface HealthReport {
|
|
|
142
170
|
* keep its previous value instead of silently flipping a session to "visible" on a partial report.
|
|
143
171
|
*/
|
|
144
172
|
export declare function readHealthEvent(data: Record<string, unknown>): HealthReport;
|
|
173
|
+
/**
|
|
174
|
+
* The ledger block for the health payload, or nothing when it is not worth saying.
|
|
175
|
+
*
|
|
176
|
+
* Absent below `LEDGER_NOTICE_FRACTION` of the cap, and absent when the size has not been measured
|
|
177
|
+
* yet -- a reader must be able to tell "not measured" from "empty", and a zero reported before the
|
|
178
|
+
* first append would be a claim rather than a reading.
|
|
179
|
+
*/
|
|
180
|
+
export declare function ledgerNotice(ledger: {
|
|
181
|
+
bytes: number;
|
|
182
|
+
capBytes: number;
|
|
183
|
+
} | undefined): {
|
|
184
|
+
ledger?: {
|
|
185
|
+
bytes: number;
|
|
186
|
+
capBytes: number;
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Every health field that is present only when it means something.
|
|
191
|
+
*
|
|
192
|
+
* One place answers "which optional facts belong on the health block", so a session does not have
|
|
193
|
+
* to know the rule for each. A healthy session gets an empty object back and its health block
|
|
194
|
+
* carries only the fields that are always true.
|
|
195
|
+
*/
|
|
196
|
+
export declare function healthNotices(events: readonly ReticleEvent[], elapsed: number, ledger: {
|
|
197
|
+
bytes: number;
|
|
198
|
+
capBytes: number;
|
|
199
|
+
} | undefined): {
|
|
200
|
+
pendingNavigationMs?: number;
|
|
201
|
+
ledger?: {
|
|
202
|
+
bytes: number;
|
|
203
|
+
capBytes: number;
|
|
204
|
+
};
|
|
205
|
+
};
|
|
145
206
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BrowserBrand, EventType, BUFFER_EVICTION_WARNING, THROTTLED_STARVED_NOTE, THROTTLED_WARNING, } from '@reticlehq/core';
|
|
1
|
+
import { BrowserBrand, EventType, BUFFER_EVICTION_WARNING, THROTTLED_STARVED_NOTE, SESSION_HEALTH, THROTTLED_WARNING, } from '@reticlehq/core';
|
|
2
2
|
/**
|
|
3
3
|
* How long a request may be in flight before it is worth reporting on session health.
|
|
4
4
|
*
|
|
@@ -68,6 +68,16 @@ export function bufferEnvelope(session) {
|
|
|
68
68
|
return {};
|
|
69
69
|
return { buffer: { held: total, dropped, note: BUFFER_EVICTION_WARNING } };
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The session registered under `session.id` NOW, for describing a tab after a call that may have
|
|
73
|
+
* replaced its document. A reload or full navigation registers a new Session under the same id, and
|
|
74
|
+
* the object a handler resolved first is the page that unloaded; it reported itself hidden on the way
|
|
75
|
+
* out, so its health said "throttled" about a tab that was fine. Optional-called because test doubles
|
|
76
|
+
* are partial registries.
|
|
77
|
+
*/
|
|
78
|
+
export function currentOf(registry, session) {
|
|
79
|
+
return registry.get?.(session.id) ?? session;
|
|
80
|
+
}
|
|
71
81
|
/**
|
|
72
82
|
* Build the health envelope for a tool result. When the session is **nominal** (focused, not
|
|
73
83
|
* throttled, no escape-hatch recommendation) the block is OMITTED entirely — a healthy session
|
|
@@ -152,3 +162,32 @@ export function readHealthEvent(data) {
|
|
|
152
162
|
brand: Object.values(BrowserBrand).find((known) => known === data['brand']),
|
|
153
163
|
};
|
|
154
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* The ledger block for the health payload, or nothing when it is not worth saying.
|
|
167
|
+
*
|
|
168
|
+
* Absent below `LEDGER_NOTICE_FRACTION` of the cap, and absent when the size has not been measured
|
|
169
|
+
* yet -- a reader must be able to tell "not measured" from "empty", and a zero reported before the
|
|
170
|
+
* first append would be a claim rather than a reading.
|
|
171
|
+
*/
|
|
172
|
+
export function ledgerNotice(ledger) {
|
|
173
|
+
if (ledger === undefined || 0 >= ledger.capBytes)
|
|
174
|
+
return {};
|
|
175
|
+
const fraction = ledger.bytes / ledger.capBytes;
|
|
176
|
+
return fraction < SESSION_HEALTH.LEDGER_NOTICE_FRACTION ? {} : { ledger };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Every health field that is present only when it means something.
|
|
180
|
+
*
|
|
181
|
+
* One place answers "which optional facts belong on the health block", so a session does not have
|
|
182
|
+
* to know the rule for each. A healthy session gets an empty object back and its health block
|
|
183
|
+
* carries only the fields that are always true.
|
|
184
|
+
*/
|
|
185
|
+
export function healthNotices(events, elapsed, ledger) {
|
|
186
|
+
// From event t=0, not a cursor: a wedge that began before the current action is exactly the case
|
|
187
|
+
// a per-window reading cannot see, and is the one both reporters hit.
|
|
188
|
+
const stuck = pendingNavigationMs(events, elapsed);
|
|
189
|
+
return {
|
|
190
|
+
...(stuck === undefined ? {} : { pendingNavigationMs: stuck }),
|
|
191
|
+
...ledgerNotice(ledger),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -45,6 +45,12 @@ export interface SessionInfo {
|
|
|
45
45
|
* A tab that dropped for four seconds and came back is indistinguishable from one that never left
|
|
46
46
|
* (#117), and that difference decides whether a verdict over the window can be trusted. Attached
|
|
47
47
|
* by `SessionManager.list()`, which is the layer that sees every connect and disconnect.
|
|
48
|
+
*
|
|
49
|
+
* Scoped to the SDK-to-daemon LINK, and the names do not say so on their own. A zombie tab whose
|
|
50
|
+
* dev server had died reported `outages: 0` and `connectedSinceMs: 11min`, and both were correct:
|
|
51
|
+
* the page's socket was still attached to the daemon. Read as app health, they said the app was
|
|
52
|
+
* up while nothing was listening on the port (#938). The describe() on the `reticle_sessions`
|
|
53
|
+
* output schema carries that caveat, because that is where an agent reads it.
|
|
48
54
|
*/
|
|
49
55
|
attachment?: {
|
|
50
56
|
connectedSinceMs: number;
|
|
@@ -22,6 +22,15 @@ export declare class SessionManager {
|
|
|
22
22
|
add(session: Session): Session | undefined;
|
|
23
23
|
remove(session: Session): boolean;
|
|
24
24
|
get(sessionId: string): Session | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Did this id belong to a session that has since disconnected?
|
|
27
|
+
*
|
|
28
|
+
* `resolve` answers a departed id with its live SUCCESSOR, which is what keeps a reload working
|
|
29
|
+
* for everything that observes. A caller that is about to do something DESTRUCTIVE needs the
|
|
30
|
+
* other answer: whether the thing it named is still there. Ending a session it did not name is
|
|
31
|
+
* not a recoverable mistake (#983).
|
|
32
|
+
*/
|
|
33
|
+
departed(sessionId: string): SessionIdentity | undefined;
|
|
25
34
|
list(): SessionInfo[];
|
|
26
35
|
/** Continuity for one session — "has this been attached the whole time I am reasoning about?" */
|
|
27
36
|
attachmentOf(sessionId: string): ReturnType<AttachmentHistory['of']>;
|
|
@@ -3,7 +3,7 @@ import { notePendingNoSessionReason } from '../../telemetry/tool-refused.js';
|
|
|
3
3
|
import { declareDrivenRedactionKeys, forgetDrivenRedactionKeys, } from '../input/driven-redaction.js';
|
|
4
4
|
import { Session } from './session.js';
|
|
5
5
|
import { AttachmentHistory } from './presence/attachment-history.js';
|
|
6
|
-
import { pickDocumentSuccessor } from './session-successor.js';
|
|
6
|
+
import { openedBeforeDeparture, pickDocumentSuccessor, } from './session-successor.js';
|
|
7
7
|
import { SESSION_DISCONNECTED_REASON } from './facts/session-replaced.js';
|
|
8
8
|
/** The scheme://host:port of a URL, or undefined if it can't be parsed. Used to compare origins. */
|
|
9
9
|
export function originOf(url) {
|
|
@@ -87,6 +87,8 @@ export class SessionManager {
|
|
|
87
87
|
* id forgets its tombstone — the live session is the answer then.
|
|
88
88
|
*/
|
|
89
89
|
#tombstones = new Map();
|
|
90
|
+
/** The tabs already open when each tombstoned session departed: siblings, never its successor. */
|
|
91
|
+
#siblingsAtDeparture = new Map();
|
|
90
92
|
/**
|
|
91
93
|
* The active project's scope, set once from the daemon's .reticle.json. When a tool resolves a session
|
|
92
94
|
* without passing its own scope, this is applied — so auto-selection is project-scoped by default
|
|
@@ -128,6 +130,7 @@ export class SessionManager {
|
|
|
128
130
|
declareDrivenRedactionKeys(session.id, session.redactKeys);
|
|
129
131
|
this.#attachment.attached(session.id);
|
|
130
132
|
this.#tombstones.delete(session.id);
|
|
133
|
+
this.#siblingsAtDeparture.delete(session.id);
|
|
131
134
|
return previous;
|
|
132
135
|
}
|
|
133
136
|
/**
|
|
@@ -158,6 +161,9 @@ export class SessionManager {
|
|
|
158
161
|
return;
|
|
159
162
|
if (this.#tombstones.has(session.id))
|
|
160
163
|
this.#tombstones.delete(session.id);
|
|
164
|
+
this.#siblingsAtDeparture.set(session.id, new Set([...this.#sessions.values()]
|
|
165
|
+
.filter((s) => s !== session && openedBeforeDeparture(s, session))
|
|
166
|
+
.map((s) => s.id)));
|
|
161
167
|
this.#tombstones.set(session.id, {
|
|
162
168
|
id: session.id,
|
|
163
169
|
url: session.url,
|
|
@@ -168,13 +174,17 @@ export class SessionManager {
|
|
|
168
174
|
if (oldest === undefined)
|
|
169
175
|
break;
|
|
170
176
|
this.#tombstones.delete(oldest);
|
|
177
|
+
this.#siblingsAtDeparture.delete(oldest);
|
|
171
178
|
}
|
|
172
179
|
}
|
|
173
180
|
#successorOf(sessionId) {
|
|
174
181
|
const departed = this.#tombstones.get(sessionId);
|
|
175
182
|
if (departed === undefined)
|
|
176
183
|
return undefined;
|
|
177
|
-
const
|
|
184
|
+
const siblings = this.#siblingsAtDeparture.get(sessionId);
|
|
185
|
+
const picked = pickDocumentSuccessor(this.all()
|
|
186
|
+
.filter((s) => true !== siblings?.has(s.id))
|
|
187
|
+
.map((s) => ({
|
|
178
188
|
id: s.id,
|
|
179
189
|
url: s.url,
|
|
180
190
|
...(s.projectId === undefined ? {} : { projectId: s.projectId }),
|
|
@@ -186,6 +196,17 @@ export class SessionManager {
|
|
|
186
196
|
get(sessionId) {
|
|
187
197
|
return this.#sessions.get(sessionId);
|
|
188
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Did this id belong to a session that has since disconnected?
|
|
201
|
+
*
|
|
202
|
+
* `resolve` answers a departed id with its live SUCCESSOR, which is what keeps a reload working
|
|
203
|
+
* for everything that observes. A caller that is about to do something DESTRUCTIVE needs the
|
|
204
|
+
* other answer: whether the thing it named is still there. Ending a session it did not name is
|
|
205
|
+
* not a recoverable mistake (#983).
|
|
206
|
+
*/
|
|
207
|
+
departed(sessionId) {
|
|
208
|
+
return this.#tombstones.get(sessionId);
|
|
209
|
+
}
|
|
189
210
|
list() {
|
|
190
211
|
return [...this.#sessions.values()].map((s) => {
|
|
191
212
|
const info = s.info();
|
|
@@ -26,6 +26,17 @@ export interface SuccessorClock {
|
|
|
26
26
|
* A same-id reconnect is a successor: the id survived in sessionStorage, the document did not.
|
|
27
27
|
*/
|
|
28
28
|
export declare function pickDocumentSuccessor(live: readonly SessionIdentity[], departed: SessionIdentity): SessionIdentity | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Whether `candidate` was already connected before the agent last drove `departed`.
|
|
31
|
+
*
|
|
32
|
+
* Such a tab is a SIBLING, never the document the departed tab's navigation produced: that document
|
|
33
|
+
* can only have connected after the command that caused it. Without this, two tabs of one project
|
|
34
|
+
* were resolved as "unique at the origin" in the gap before the departed tab came back, and a verdict
|
|
35
|
+
* for tab A was graded on tab B (#983, reproduced by driving). Measured as ages rather than instants,
|
|
36
|
+
* so both sides read the same clock at the same moment and the comparison does not drift while a
|
|
37
|
+
* caller polls.
|
|
38
|
+
*/
|
|
39
|
+
export declare function openedBeforeDeparture(candidate: Pick<Session, 'staleMs'>, departed: Pick<Session, 'agentIdleMs'>): boolean;
|
|
29
40
|
export interface SuccessorRegistry {
|
|
30
41
|
get(id: string): Session | undefined;
|
|
31
42
|
all(): readonly Session[];
|
|
@@ -100,6 +100,19 @@ export function pickDocumentSuccessor(live, departed) {
|
|
|
100
100
|
return undefined;
|
|
101
101
|
return others[0];
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether `candidate` was already connected before the agent last drove `departed`.
|
|
105
|
+
*
|
|
106
|
+
* Such a tab is a SIBLING, never the document the departed tab's navigation produced: that document
|
|
107
|
+
* can only have connected after the command that caused it. Without this, two tabs of one project
|
|
108
|
+
* were resolved as "unique at the origin" in the gap before the departed tab came back, and a verdict
|
|
109
|
+
* for tab A was graded on tab B (#983, reproduced by driving). Measured as ages rather than instants,
|
|
110
|
+
* so both sides read the same clock at the same moment and the comparison does not drift while a
|
|
111
|
+
* caller polls.
|
|
112
|
+
*/
|
|
113
|
+
export function openedBeforeDeparture(candidate, departed) {
|
|
114
|
+
return candidate.staleMs() > departed.agentIdleMs();
|
|
115
|
+
}
|
|
103
116
|
/**
|
|
104
117
|
* Poll until a successor of `departed` is live, or the budget runs out. Null on timeout — never
|
|
105
118
|
* throws, because failing to follow is a legitimate `observation_lost`, not an error.
|
|
@@ -115,7 +128,7 @@ export async function awaitDocumentSuccessor(sessions, departed, timeoutMs, cloc
|
|
|
115
128
|
// out of the pick, or same-id-first would keep returning the page that just unloaded.
|
|
116
129
|
const picked = pickDocumentSuccessor(sessions
|
|
117
130
|
.all()
|
|
118
|
-
.filter((s) => s !== departed)
|
|
131
|
+
.filter((s) => s !== departed && !openedBeforeDeparture(s, departed))
|
|
119
132
|
.map(identityOf), departedIdentity);
|
|
120
133
|
if (picked !== undefined) {
|
|
121
134
|
const live = sessions.get(picked.id);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The facts a SESSION contributes to a verdict, in the shape `decideVerified` takes.
|
|
3
|
+
*
|
|
4
|
+
* `decideVerified` is pure and has no session, so anything it needs about the link has to be
|
|
5
|
+
* threaded in. Two tools produce verdicts — `reticle_assert` and `reticle_act_and_wait` — and both
|
|
6
|
+
* were threading the same fields with the same conditional-spread idiom and the same comment
|
|
7
|
+
* beside it. A third fact (`versionSkew`) would have been a third copy.
|
|
8
|
+
*
|
|
9
|
+
* One function instead, for the ordinary reason: when a fourth fact arrives, a verdict surface that
|
|
10
|
+
* learns it in one place cannot half-learn it. That failure mode is not hypothetical here — the
|
|
11
|
+
* repo already records `net` assertions being discarded on one half of a surface because a filter
|
|
12
|
+
* was updated on the other.
|
|
13
|
+
*
|
|
14
|
+
* Every field is omitted rather than defaulted when absent. An SDK too old to declare sends
|
|
15
|
+
* nothing, and an empty value is a claim nobody made.
|
|
16
|
+
*/
|
|
17
|
+
/** What a session can tell a verdict. Optional throughout: absent means nobody said. */
|
|
18
|
+
export interface SessionVerdictFacts {
|
|
19
|
+
readonly sdkVersion?: string;
|
|
20
|
+
readonly versionSkew?: string;
|
|
21
|
+
}
|
|
22
|
+
/** The shape read here — structural, so a test double needs no session. */
|
|
23
|
+
export interface VerdictRelevantSession {
|
|
24
|
+
readonly sdkVersion?: string | undefined;
|
|
25
|
+
readonly versionSkew?: string | undefined;
|
|
26
|
+
readonly channels?: readonly string[] | undefined;
|
|
27
|
+
}
|
|
28
|
+
export declare function sessionVerdictFacts(session: VerdictRelevantSession): SessionVerdictFacts;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function sessionVerdictFacts(session) {
|
|
2
|
+
const facts = {};
|
|
3
|
+
// So the unread-body remedy can check it applies to THIS page.
|
|
4
|
+
if (session.sdkVersion !== undefined)
|
|
5
|
+
facts.sdkVersion = session.sdkVersion;
|
|
6
|
+
/*
|
|
7
|
+
* An empty string is NOT a skew. The bridge sets this only on a real mismatch, but a defaulted
|
|
8
|
+
* or cleared field reaching the verdict would cost every call a `yes` it had earned — which is a
|
|
9
|
+
* worse failure than the one this exists to fix.
|
|
10
|
+
*/
|
|
11
|
+
if (session.versionSkew !== undefined && session.versionSkew.length > 0) {
|
|
12
|
+
facts.versionSkew = session.versionSkew;
|
|
13
|
+
}
|
|
14
|
+
return facts;
|
|
15
|
+
}
|
|
@@ -44,6 +44,11 @@ export declare class Session implements HandshakeFacts {
|
|
|
44
44
|
* root, which is what every counter did unconditionally before this existed.
|
|
45
45
|
*/
|
|
46
46
|
artifactRoot: string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Same-origin endpoints this session's project declared as background (`.reticle.json`
|
|
49
|
+
* `background`). Stamped with `artifactRoot`, from the same project. Empty excludes nothing.
|
|
50
|
+
*/
|
|
51
|
+
background: readonly string[];
|
|
47
52
|
url: string;
|
|
48
53
|
title: string;
|
|
49
54
|
adapters: string[];
|
|
@@ -5,19 +5,19 @@ import { LastAct } from './last-act.js';
|
|
|
5
5
|
import { GapLedger } from '@reticlehq/engine/evidence/gap-ledger.js';
|
|
6
6
|
import { CaptureLedger } from '../../surface/tools/feature-capture.js';
|
|
7
7
|
import { commandTimeoutMessage } from './timing/command-timeout.js';
|
|
8
|
-
import { readHealthEvent,
|
|
8
|
+
import { readHealthEvent, healthNotices } from './session-health.js';
|
|
9
9
|
import { MIRRORED_COMMANDS, mirroredNarration } from './presence/session-mirror.js';
|
|
10
10
|
/** The HUD does not need 200 frames to watch a counter climb; the last state always lands. */
|
|
11
11
|
const IMPACT_PUSH_DEBOUNCE_MS = 700;
|
|
12
12
|
import { PendingCommands, CommandTimeoutError } from './page-commands/pending-commands.js';
|
|
13
13
|
import { span } from '../../trace.js';
|
|
14
14
|
import { keepCallerContextInDaemon, noteToDaemonLog } from './daemon-answers.js';
|
|
15
|
-
import { isKnownRealm, EventType, HumanControlDataSchema, HumanControlKind, HumanMarkDataSchema, ReticleCommand, MessageKind, parseEventPayload, PresenterTone, SESSION_HEALTH, SESSION_LEASE, SESSION_LIFECYCLE, SessionState, } from '@reticlehq/core';
|
|
15
|
+
import { isKnownRealm, EventType, HumanControlDataSchema, HumanControlKind, HumanMarkDataSchema, HudUseDataSchema, ReticleCommand, MessageKind, parseEventPayload, PresenterTone, SESSION_HEALTH, SESSION_LEASE, SESSION_LIFECYCLE, SessionState, } from '@reticlehq/core';
|
|
16
16
|
import { RingBuffer } from '@reticlehq/engine/window/ring-buffer.js';
|
|
17
17
|
import { readJournalWriteLoss, readQueryEvents, } from '../../memory/journal/journal-query.js';
|
|
18
18
|
import {} from '@reticlehq/engine/window/ambient.js';
|
|
19
19
|
import { ObservedState } from './facts/observed-state.js';
|
|
20
|
-
import { recordBrowserLatency, recordSdkFailure } from '../../telemetry/session-metrics.js';
|
|
20
|
+
import { recordBrowserLatency, recordHudUse, recordSdkFailure, } from '../../telemetry/session-metrics.js';
|
|
21
21
|
import { LiveControl } from './human/live-control.js';
|
|
22
22
|
import { ReviewStore } from './human/review-store.js';
|
|
23
23
|
import { buildSessionRecommendation } from './presence/session-recommendation.js';
|
|
@@ -63,6 +63,11 @@ export class Session {
|
|
|
63
63
|
* root, which is what every counter did unconditionally before this existed.
|
|
64
64
|
*/
|
|
65
65
|
artifactRoot;
|
|
66
|
+
/**
|
|
67
|
+
* Same-origin endpoints this session's project declared as background (`.reticle.json`
|
|
68
|
+
* `background`). Stamped with `artifactRoot`, from the same project. Empty excludes nothing.
|
|
69
|
+
*/
|
|
70
|
+
background = [];
|
|
66
71
|
url;
|
|
67
72
|
title;
|
|
68
73
|
adapters;
|
|
@@ -188,17 +193,16 @@ export class Session {
|
|
|
188
193
|
}
|
|
189
194
|
/** The attachable health block — single source of truth for the tools. */
|
|
190
195
|
health() {
|
|
191
|
-
// From event t=0, not a cursor: a wedge that began before the current action is exactly the
|
|
192
|
-
// case a per-window reading cannot see, and is the one both reporters hit.
|
|
193
|
-
const stuck = pendingNavigationMs(this.eventsSince(0), this.elapsed());
|
|
194
196
|
const base = {
|
|
195
197
|
lastSeenMs: this.lastSeenMs(),
|
|
196
198
|
throttled: this.throttled(),
|
|
197
199
|
focused: this.#focused,
|
|
198
|
-
...(stuck === undefined ? {} : { pendingNavigationMs: stuck }),
|
|
199
200
|
// Carried onto every act/assert result, not just reticle_sessions: skew drops actions
|
|
200
201
|
// SILENTLY, and the fields it contradicts are on the act verdict.
|
|
201
202
|
...(this.versionSkew === undefined ? {} : { versionSkew: this.versionSkew }),
|
|
203
|
+
// The absence-gated facts: a stuck navigation and an ledger worth naming. Both are absent on
|
|
204
|
+
// a healthy session, and `healthNotices` owns which is which.
|
|
205
|
+
...healthNotices(this.eventsSince(0), this.elapsed(), this.#journalReader?.ledger?.()),
|
|
202
206
|
};
|
|
203
207
|
// attach the escape-hatch hint only when un-scriptable (keeps field absent otherwise).
|
|
204
208
|
const recommendation = buildSessionRecommendation({
|
|
@@ -261,6 +265,14 @@ export class Session {
|
|
|
261
265
|
keepCallerContext = keepCallerContextInDaemon;
|
|
262
266
|
/** Re-stamp an incoming event with server-relative time, buffer it, and fan out. */
|
|
263
267
|
pushEvent(event, byteSize) {
|
|
268
|
+
// Telemetry about the HUD, not evidence about the app: taken off here, before the buffer, so no
|
|
269
|
+
// verdict, settle window or agent read can ever see a person's clicks on Reticle's own panel.
|
|
270
|
+
if (event.type === EventType.HUD_USED) {
|
|
271
|
+
const use = HudUseDataSchema.safeParse(event.data);
|
|
272
|
+
if (use.success)
|
|
273
|
+
recordHudUse(use.data);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
264
276
|
if (event.documentId !== undefined)
|
|
265
277
|
this.#documentId = event.documentId;
|
|
266
278
|
if (event.editEpoch !== undefined)
|
|
@@ -10,9 +10,9 @@ export { probeDaemon, waitForDaemon } from './proxy/proxy-daemon-probe.js';
|
|
|
10
10
|
import { probeDaemon } from './proxy/proxy-daemon-probe.js';
|
|
11
11
|
import { SERVER_VERSION } from '../../command/version/identity/server-version.js';
|
|
12
12
|
import { buildServerInstructions } from './server-instructions.js';
|
|
13
|
-
import {
|
|
13
|
+
import { hasAnyProjectConnectedBefore } from '../../memory/recall/prior/connection-memory.js';
|
|
14
14
|
import { reticleStateHome } from '../../command/daemon/daemon.js';
|
|
15
|
-
import {
|
|
15
|
+
import { projectIdsAt } from '../../command/cli/ports/resolve/cli-port.js';
|
|
16
16
|
import { PEER_VERSION_PARAM, PEER_CONTRACT_PARAM } from '../../command/version/peer-announce.js';
|
|
17
17
|
import { onStreamDrop, onClientRequest, onReconnectBudgetSpent, onQueueExpired, OnDrop, OnRequest, } from './proxy/proxy-lifecycle.js';
|
|
18
18
|
import { describePresence, probePresence } from '../../command/daemon/binding/port-presence.js';
|
|
@@ -289,7 +289,7 @@ export function buildSessionUrl(rawData, port) {
|
|
|
289
289
|
function proxyInstructions(port) {
|
|
290
290
|
try {
|
|
291
291
|
return buildServerInstructions({
|
|
292
|
-
previouslyConnected:
|
|
292
|
+
previouslyConnected: hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd())),
|
|
293
293
|
});
|
|
294
294
|
}
|
|
295
295
|
catch {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export declare const PROXY_IDLE_EXIT_MS: number;
|
|
11
11
|
/** The local diagnostic emitted before an abandoned proxy exits. */
|
|
12
12
|
export declare const PROXY_IDLE_EXIT_EVENT = "reticle_mcp_proxy_idle_exit";
|
|
13
|
-
|
|
13
|
+
interface ProxyIdleExitOptions {
|
|
14
14
|
/** Called once after the client link has stayed quiet for the whole grace. */
|
|
15
15
|
onExit: (idleMs: number) => void;
|
|
16
16
|
/** True while a request or queued message still needs an answer. */
|
|
@@ -43,3 +43,4 @@ export declare class ProxyIdleExit {
|
|
|
43
43
|
* zero as the explicit opt-out. Invalid values retain the conservative one-day default.
|
|
44
44
|
*/
|
|
45
45
|
export declare function resolveProxyIdleExitMs(raw: string | undefined): number;
|
|
46
|
+
export {};
|
|
@@ -31,7 +31,18 @@ import { surfaceVocabulary, listOf } from './surface-vocabulary.js';
|
|
|
31
31
|
* measured reason. A briefing that names a tool the agent was not given does not merely confuse it;
|
|
32
32
|
* it makes the agent stop using the product altogether.
|
|
33
33
|
*/
|
|
34
|
-
|
|
34
|
+
/*
|
|
35
|
+
* `reticle gate` is FOLDED INTO the done sentence rather than given a paragraph, and that is the
|
|
36
|
+
* budget rule working rather than a stylistic choice.
|
|
37
|
+
*
|
|
38
|
+
* The instruction block has a hard byte cap whose own note says a raise must be paid for with a
|
|
39
|
+
* measurement, and that if one cannot be shown the right answer is to cut an older sentence. There
|
|
40
|
+
* is no measurement for `gate`: it is built, it has anti-reward-hacking, and it appeared in no
|
|
41
|
+
* surface an agent reads, which is an argument and not a number. Cutting a measured sentence to
|
|
42
|
+
* make room for an unmeasured one would be strictly worse. So it costs about a hundred characters
|
|
43
|
+
* inside the sentence that already names the exact moment it applies to, and the cap did not move.
|
|
44
|
+
*/
|
|
45
|
+
const verdictDiscipline = (v) => `Reticle verifies a running web app from the inside: go (${v.navigate}), look (${listOf(v.look, v.find)}), act and prove in one hop (${v.actAndWait}), observe (${listOf(v.observe, v.state, v.network, v.console)}), assert (${v.assert}). Verify a user-facing change against the real app before you call it done — \`npx @reticlehq/server gate --since <ref>\` fails unless a passing artifact covers every flow your edits affect — and never weaken a check to make it pass.
|
|
35
46
|
|
|
36
47
|
Only ${v.actAndWait} and ${v.assert} produce a verdict. ${v.act.length > 0 ? `${v.act} and e` : 'E'}verything else moves or reads the app and proves nothing, so a drive that ends without one of those two has no result however many tools it used. Prefer ${v.actAndWait}({ ref, action, until }) — it names the expected consequence BEFORE the action, which is the difference between a check and a rationalisation. Only verified:"yes" is a pass — "unknown" means Reticle could not tell what happened, "no-fault" means nothing was declared to prove. Report either as not proved.
|
|
37
48
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Predicate } from '@reticlehq/engine/question/predicate/predicate-schema.js';
|
|
2
|
+
import type { PredicateSession } from '@reticlehq/engine/question/predicate/predicate-session.js';
|
|
3
|
+
import type { Baselines } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
4
|
+
/** Everything the pre-action check learned, which is three facts about one reading. */
|
|
5
|
+
export interface AlreadyTrueReading {
|
|
6
|
+
/** The consequence held BEFORE the action, so the action proved nothing about it. */
|
|
7
|
+
alreadyTrue: boolean;
|
|
8
|
+
/** That pre-existing match was against something hidden — a stronger reason to doubt the claim. */
|
|
9
|
+
alreadyTrueHiddenMatch: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* WHAT was already true, kept rather than discarded.
|
|
12
|
+
*
|
|
13
|
+
* `already_true` tells an agent its assertion held before it acted; the next question is always
|
|
14
|
+
* "true how?" and it was unanswerable, because the reading decided the bit and was then thrown
|
|
15
|
+
* away. Afterwards the action has run and the state may have moved, so the one moment that
|
|
16
|
+
* mattered is gone. Present only when the answer is yes: a verdict that was genuinely caused
|
|
17
|
+
* carries its own evidence and needs no baseline beside it.
|
|
18
|
+
*/
|
|
19
|
+
alreadyTrueEvidence?: unknown;
|
|
20
|
+
/**
|
|
21
|
+
* What each comparing leaf was reading BEFORE the action — the predicate language's past tense.
|
|
22
|
+
*
|
|
23
|
+
* Taken here because this is already the one place that looks at the app before it is touched,
|
|
24
|
+
* and a second pre-action pass would be a second round trip for a reading we are standing next
|
|
25
|
+
* to. Empty unless the caller actually asked for a comparison, so nothing pays for it.
|
|
26
|
+
*/
|
|
27
|
+
baselines: Baselines;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Ask whether the declared consequence was already true, once, before the action.
|
|
31
|
+
*
|
|
32
|
+
* Only for predicates that read live DOM state. Event-based ones are floored at this act's cursor
|
|
33
|
+
* and cannot be satisfied by the past, so they need no pre-check and pay nothing. One extra query,
|
|
34
|
+
* on the path where a green is otherwise unfalsifiable.
|
|
35
|
+
*
|
|
36
|
+
* Gathered here rather than as three locals at the call site because they are three readings OF ONE
|
|
37
|
+
* THING, and `act-tools.ts` sits on the 1000-line cohesion cap: the rule says split before adding,
|
|
38
|
+
* and this is the unit that wanted splitting.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readAlreadyTrue(session: PredicateSession, until: Predicate | undefined, since: number): Promise<AlreadyTrueReading>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { evaluatePredicate } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
2
|
+
import { readsDomState } from '@reticlehq/engine/evidence/already-true.js';
|
|
3
|
+
import { alreadyTrueHiddenMatch as alreadyTrueHiddenMatchOf } from '@reticlehq/engine/evidence/already-true.js';
|
|
4
|
+
import { captureBaselines } from '@reticlehq/engine/evidence/baseline.js';
|
|
5
|
+
/**
|
|
6
|
+
* Ask whether the declared consequence was already true, once, before the action.
|
|
7
|
+
*
|
|
8
|
+
* Only for predicates that read live DOM state. Event-based ones are floored at this act's cursor
|
|
9
|
+
* and cannot be satisfied by the past, so they need no pre-check and pay nothing. One extra query,
|
|
10
|
+
* on the path where a green is otherwise unfalsifiable.
|
|
11
|
+
*
|
|
12
|
+
* Gathered here rather than as three locals at the call site because they are three readings OF ONE
|
|
13
|
+
* THING, and `act-tools.ts` sits on the 1000-line cohesion cap: the rule says split before adding,
|
|
14
|
+
* and this is the unit that wanted splitting.
|
|
15
|
+
*/
|
|
16
|
+
export async function readAlreadyTrue(session, until, since) {
|
|
17
|
+
// Before the pre-check, because the pre-check EVALUATES the predicate and a relative leaf with no
|
|
18
|
+
// baseline yet would report `inconclusive` — reading the app twice to answer a question we had
|
|
19
|
+
// not taken the reading for.
|
|
20
|
+
const baselines = await captureBaselines(session, until);
|
|
21
|
+
const precheck = until !== undefined && readsDomState(until)
|
|
22
|
+
? await evaluatePredicate(session, until, since, false, baselines)
|
|
23
|
+
: undefined;
|
|
24
|
+
const alreadyTrue = precheck?.pass ?? false;
|
|
25
|
+
if (!alreadyTrue || until === undefined) {
|
|
26
|
+
return { alreadyTrue: false, alreadyTrueHiddenMatch: false, baselines };
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
alreadyTrue: true,
|
|
30
|
+
baselines,
|
|
31
|
+
// The pre-check evidence is already in hand, so asking whether the match was against something
|
|
32
|
+
// hidden costs nothing and lets the message name it (#889).
|
|
33
|
+
alreadyTrueHiddenMatch: alreadyTrueHiddenMatchOf(until, precheck?.evidence),
|
|
34
|
+
...(precheck?.evidence === undefined ? {} : { alreadyTrueEvidence: precheck.evidence }),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -24,6 +24,12 @@ import type { NavigateArrival } from './navigate-result.js';
|
|
|
24
24
|
* and the SDK to dial back; short enough to not hang. A caller who knows the app is slower says so.
|
|
25
25
|
*/
|
|
26
26
|
export declare const ARRIVAL_TIMEOUT_MS = 5000;
|
|
27
|
+
/**
|
|
28
|
+
* The navigation target as an absolute URL, resolved against the tab being navigated — the same
|
|
29
|
+
* resolution the browser applies. An agent passes a path (`/orders?id=4`) as often as a full URL,
|
|
30
|
+
* and `samePage` cannot parse a path, so without this a relative navigation could never confirm.
|
|
31
|
+
*/
|
|
32
|
+
export declare function absoluteTarget(target: string, base: string): string;
|
|
27
33
|
/**
|
|
28
34
|
* Which session an arrival may be attributed to.
|
|
29
35
|
*
|
|
@@ -41,6 +41,19 @@ function samePage(a, b) {
|
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The navigation target as an absolute URL, resolved against the tab being navigated — the same
|
|
46
|
+
* resolution the browser applies. An agent passes a path (`/orders?id=4`) as often as a full URL,
|
|
47
|
+
* and `samePage` cannot parse a path, so without this a relative navigation could never confirm.
|
|
48
|
+
*/
|
|
49
|
+
export function absoluteTarget(target, base) {
|
|
50
|
+
try {
|
|
51
|
+
return new URL(target, base).href;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return target;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
44
57
|
/**
|
|
45
58
|
* The sessions already on `target` — the set `ArrivalScope.priorIds` is built from, sampled before
|
|
46
59
|
* the navigation is dispatched.
|
|
@@ -34,6 +34,10 @@ export interface SettleSource {
|
|
|
34
34
|
data: Record<string, unknown>;
|
|
35
35
|
}[];
|
|
36
36
|
elapsed(): number;
|
|
37
|
+
/** The page under test — what makes a request third-party. Absent: nothing is foreign. */
|
|
38
|
+
url?: string;
|
|
39
|
+
/** Same-origin endpoints the project declared as background. Absent: nothing is excluded. */
|
|
40
|
+
background?: readonly string[];
|
|
37
41
|
}
|
|
38
42
|
interface WaitOpts {
|
|
39
43
|
/** Injected so the wait is testable without real time passing (rule: never call the clock inline). */
|
|
@@ -49,7 +53,7 @@ interface WaitOpts {
|
|
|
49
53
|
export declare function inFlightRequestIds(events: readonly {
|
|
50
54
|
type: string;
|
|
51
55
|
data: Record<string, unknown>;
|
|
52
|
-
}[]): string[];
|
|
56
|
+
}[], appOrigin?: string, background?: readonly string[]): string[];
|
|
53
57
|
/**
|
|
54
58
|
* The same outstanding requests, as "METHOD url" — what an unsettled verdict has to be able to name.
|
|
55
59
|
*
|
|
@@ -61,7 +65,7 @@ export declare function inFlightRequestIds(events: readonly {
|
|
|
61
65
|
export declare function inFlightRequestLabels(events: readonly {
|
|
62
66
|
type: string;
|
|
63
67
|
data: Record<string, unknown>;
|
|
64
|
-
}[]): string[];
|
|
68
|
+
}[], appOrigin?: string, background?: readonly string[]): string[];
|
|
65
69
|
/**
|
|
66
70
|
* Endpoints called MORE THAN ONCE in this window, busiest first, as "METHOD url ×N".
|
|
67
71
|
*
|