@reticlehq/server 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-ENTERPRISE +1 -1
- package/README.md +3 -3
- package/SKILL.md +12 -14
- package/dist/chromium-launch-options.d.ts +4 -3
- package/dist/chromium-launch-options.js +6 -2
- package/dist/command/cli/adhoc-suite.d.ts +22 -0
- package/dist/command/cli/adhoc-suite.js +90 -19
- package/dist/command/cli/adhoc-verdict.d.ts +35 -0
- package/dist/command/cli/adhoc-verdict.js +90 -3
- package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
- package/dist/command/cli/answers/gate-hook-message.js +6 -0
- package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
- package/dist/command/cli/auth/cloud-keystore.js +0 -3
- package/dist/command/cli/auth/cloud-session.d.ts +4 -0
- package/dist/command/cli/auth/cloud-session.js +2 -0
- package/dist/command/cli/cli-doctor.js +7 -5
- package/dist/command/cli/cli-flow-commands.d.ts +3 -6
- package/dist/command/cli/cli-flow-commands.js +67 -4
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +21 -3
- package/dist/command/cli/cli-update-commands.js +8 -1
- package/dist/command/cli/cli-verify.d.ts +24 -3
- package/dist/command/cli/cli-verify.js +54 -11
- package/dist/command/cli/cloud-cli.js +5 -2
- package/dist/command/cli/cloud-kit.js +3 -5
- package/dist/command/cli/cloud-login.js +21 -3
- package/dist/command/cli/demo/demo-page.js +6 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
- package/dist/command/cli/launch/cli-launch.d.ts +8 -0
- package/dist/command/cli/launch/cli-launch.js +21 -9
- package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
- package/dist/command/cli/launch/sdk-version-match.js +206 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
- package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
- package/dist/command/cli/ports/resolve/cli-port.js +125 -15
- package/dist/command/cli/setup-mcp-cli.js +13 -13
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +21 -7
- package/dist/command/cli.js +62 -10
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
- package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
- package/dist/command/daemon/daemon-resolve.d.ts +9 -0
- package/dist/command/daemon/daemon-resolve.js +1 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
- package/dist/command/dev/stale-issue-guard.d.ts +3 -4
- package/dist/command/dev/stale-issue-guard.js +8 -8
- package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
- package/dist/command/setup/bringup/connect-lease.js +38 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
- package/dist/command/setup/bringup/dev-server-files.js +131 -0
- package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
- package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
- package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
- package/dist/command/setup/bringup/ensure-daemon.js +135 -16
- package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
- package/dist/command/setup/bringup/static-page-server.js +151 -0
- package/dist/command/setup/desktop-shape.d.ts +4 -0
- package/dist/command/setup/desktop-shape.js +18 -2
- package/dist/command/setup/init/init-port.d.ts +15 -0
- package/dist/command/setup/init/init-port.js +44 -0
- package/dist/command/setup/init/init-runtime.d.ts +1 -1
- package/dist/command/setup/init/init-runtime.js +39 -2
- package/dist/command/setup/node-effects.d.ts +27 -14
- package/dist/command/setup/node-effects.js +80 -44
- package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
- package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
- package/dist/command/setup/probe/page-probe.d.ts +13 -2
- package/dist/command/setup/probe/page-probe.js +18 -3
- package/dist/command/setup/run-setup.d.ts +26 -1
- package/dist/command/setup/run-setup.js +96 -13
- package/dist/command/setup/session-pick.d.ts +13 -0
- package/dist/command/setup/session-pick.js +20 -0
- package/dist/command/setup/setup-command.d.ts +2 -14
- package/dist/command/setup/setup-command.js +105 -31
- package/dist/command/setup/setup-mcp.js +3 -3
- package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
- package/dist/command/setup/terminal/wait-progress.js +17 -0
- package/dist/features/crawl/crawl-empty.js +1 -1
- package/dist/features/crawl/crawl-tools.d.ts +6 -0
- package/dist/features/crawl/crawl-tools.js +57 -0
- package/dist/features/crawl/crawl.js +171 -157
- package/dist/features/exhaust/app-coverage.d.ts +33 -0
- package/dist/features/exhaust/app-coverage.js +31 -0
- package/dist/features/exhaust/code-coverage.d.ts +47 -0
- package/dist/features/exhaust/code-coverage.js +77 -0
- package/dist/features/exhaust/exhaust-run.d.ts +33 -0
- package/dist/features/exhaust/exhaust-run.js +82 -0
- package/dist/features/exhaust/explorer.d.ts +103 -0
- package/dist/features/exhaust/explorer.js +195 -0
- package/dist/features/exhaust/failure-branches.d.ts +33 -0
- package/dist/features/exhaust/failure-branches.js +92 -0
- package/dist/features/exhaust/ledger.d.ts +202 -0
- package/dist/features/exhaust/ledger.js +252 -0
- package/dist/features/exhaust/session-fold.d.ts +31 -0
- package/dist/features/exhaust/session-fold.js +79 -0
- package/dist/features/exhaust/session-port.d.ts +36 -0
- package/dist/features/exhaust/session-port.js +102 -0
- package/dist/features/harness/drive-report.d.ts +0 -9
- package/dist/features/harness/drive-report.js +18 -0
- package/dist/features/harness/driver.js +4 -7
- package/dist/features/harness/fill-values.d.ts +6 -0
- package/dist/features/harness/fill-values.js +3 -0
- package/dist/features/harness/jev-driver.js +30 -13
- package/dist/features/harness/openai-driver.js +4 -7
- package/dist/features/harness/platform-config.js +5 -6
- package/dist/index.d.ts +9 -0
- package/dist/index.js +40 -11
- package/dist/judgement/domain/domain-model.js +3 -1
- package/dist/judgement/oracles/self-instrument.js +4 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
- package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
- package/dist/judgement/runs/verification-sync.d.ts +3 -1
- package/dist/judgement/runs/verification-sync.js +1 -1
- package/dist/language/flows/change/affected.d.ts +9 -0
- package/dist/language/flows/change/affected.js +35 -0
- package/dist/language/flows/change/gate.d.ts +20 -0
- package/dist/language/flows/change/gate.js +12 -1
- package/dist/language/flows/flow-author.d.ts +7 -0
- package/dist/language/flows/flow-author.js +19 -0
- package/dist/language/flows/flow-expect-grammar.js +1 -1
- package/dist/language/flows/flow-journey.d.ts +54 -0
- package/dist/language/flows/flow-journey.js +119 -0
- package/dist/language/flows/flow-replay-run.d.ts +2 -3
- package/dist/language/flows/flow-replay-run.js +13 -47
- package/dist/language/flows/flow-replay.d.ts +22 -8
- package/dist/language/flows/flow-replay.js +55 -9
- package/dist/language/flows/flow-result.d.ts +2 -0
- package/dist/language/flows/flow-result.js +25 -0
- package/dist/language/flows/flow-tools.js +17 -5
- package/dist/language/flows/flows.d.ts +10 -1
- package/dist/language/flows/flows.js +90 -14
- package/dist/language/flows/recording/replay-halt.js +3 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
- package/dist/language/flows/recording/tape/recordings.js +55 -1
- package/dist/language/flows/replay.d.ts +19 -0
- package/dist/language/flows/replay.js +42 -6
- package/dist/language/flows/verify-change-tools.js +5 -2
- package/dist/launch-chromium.d.ts +80 -0
- package/dist/launch-chromium.js +218 -0
- package/dist/memory/cloud/cloud-config.d.ts +11 -2
- package/dist/memory/cloud/cloud-config.js +47 -7
- package/dist/memory/cloud/cloud-sync.d.ts +4 -1
- package/dist/memory/cloud/cloud-sync.js +6 -9
- package/dist/memory/cloud/harness-offer.js +5 -6
- package/dist/memory/cloud/harness-switch.js +5 -6
- package/dist/memory/cloud/sync-cycle.d.ts +68 -3
- package/dist/memory/cloud/sync-cycle.js +298 -55
- package/dist/memory/cloud/sync-daemon.js +24 -20
- package/dist/memory/cloud/sync-disk.js +16 -2
- package/dist/memory/intent/intent-shard.d.ts +10 -32
- package/dist/memory/intent/intent-shard.js +0 -10
- package/dist/memory/journal/drive-flow.d.ts +18 -13
- package/dist/memory/journal/drive-flow.js +45 -37
- package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
- package/dist/memory/journal/session-end.d.ts +9 -1
- package/dist/memory/journal/session-end.js +24 -12
- package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
- package/dist/memory/project/dir/reticle-dir.js +1 -0
- package/dist/memory/project/project-tools.js +3 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
- package/dist/portal/bridge/auth-failure-reason.js +4 -2
- package/dist/portal/bridge/bridge.d.ts +8 -0
- package/dist/portal/bridge/bridge.js +43 -2
- package/dist/portal/input/js-coverage.d.ts +25 -0
- package/dist/portal/input/js-coverage.js +30 -0
- package/dist/portal/input/real-input.d.ts +11 -0
- package/dist/portal/input/real-input.js +29 -4
- package/dist/portal/pool/playwright-launcher.d.ts +6 -0
- package/dist/portal/pool/playwright-launcher.js +20 -13
- package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
- package/dist/portal/session/dev-server/dev-command.js +30 -12
- package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
- package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
- package/dist/portal/session/dev-server/route-status-probe.js +57 -0
- package/dist/portal/session/facts/observed-state.d.ts +11 -0
- package/dist/portal/session/facts/observed-state.js +34 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
- package/dist/portal/session/no-session-diagnosis.js +62 -5
- package/dist/portal/session/no-session-next-action.d.ts +13 -0
- package/dist/portal/session/no-session-next-action.js +48 -9
- package/dist/portal/session/no-session-watch.d.ts +18 -0
- package/dist/portal/session/no-session-watch.js +88 -2
- package/dist/portal/session/session-health.js +3 -2
- package/dist/portal/session/session-manager.d.ts +20 -6
- package/dist/portal/session/session-manager.js +19 -3
- package/dist/portal/session/session-reaper.d.ts +28 -0
- package/dist/portal/session/session-reaper.js +49 -1
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +15 -19
- package/dist/surface/http-server.d.ts +2 -1
- package/dist/surface/http-server.js +4 -2
- package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
- package/dist/surface/mcp/mcp-proxy.js +2 -2
- package/dist/surface/mcp/mcp.js +4 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
- package/dist/surface/mcp/server-instructions.js +9 -7
- package/dist/surface/tools/act/after-match.d.ts +17 -0
- package/dist/surface/tools/act/after-match.js +30 -0
- package/dist/surface/tools/act/durable.d.ts +69 -0
- package/dist/surface/tools/act/durable.js +139 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
- package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
- package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
- package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
- package/dist/surface/tools/act/proved-controls.d.ts +9 -0
- package/dist/surface/tools/act/proved-controls.js +11 -0
- package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
- package/dist/surface/tools/act/sequence-grade.js +32 -1
- package/dist/surface/tools/act-capsule.d.ts +7 -0
- package/dist/surface/tools/act-capsule.js +46 -1
- package/dist/surface/tools/act-sequence-tool.js +25 -8
- package/dist/surface/tools/act-tools.js +26 -50
- package/dist/surface/tools/assert/assert-grade.js +12 -1
- package/dist/surface/tools/assert/assert-verdict.js +2 -2
- package/dist/surface/tools/browser-tools.js +14 -2
- package/dist/surface/tools/coverage-tools.js +27 -1
- package/dist/surface/tools/error-recovery.js +4 -4
- package/dist/surface/tools/feedback-invite.js +6 -4
- package/dist/surface/tools/harness-explore.d.ts +3 -2
- package/dist/surface/tools/harness-explore.js +7 -6
- package/dist/surface/tools/lease-tools.js +15 -6
- package/dist/surface/tools/live-call-text.js +4 -1
- package/dist/surface/tools/observe-tools.js +9 -1
- package/dist/surface/tools/tools.js +4 -0
- package/dist/telemetry/argument-shape.d.ts +0 -2
- package/dist/telemetry/argument-shape.js +0 -4
- package/dist/telemetry/error-class.d.ts +2 -3
- package/dist/telemetry/error-class.js +2 -3
- package/dist/telemetry/harness-drive.d.ts +0 -2
- package/dist/telemetry/harness-drive.js +0 -4
- package/dist/wire-journal.d.ts +3 -0
- package/dist/wire-journal.js +4 -0
- package/dist/workspace-packages.d.ts +0 -2
- package/dist/workspace-packages.js +1 -9
- package/docs/README.md +4 -2
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +4 -4
- package/docs/architecture.md +6 -6
- package/docs/best-practices.mdx +1 -1
- package/docs/capabilities.mdx +32 -1
- package/docs/cli/cloud.mdx +14 -4
- package/docs/cli/doctor.mdx +4 -0
- package/docs/cli/feedback.mdx +2 -2
- package/docs/cli/gate.mdx +9 -4
- package/docs/cli/update.mdx +10 -0
- package/docs/cli.mdx +9 -0
- package/docs/desktop.mdx +14 -10
- package/docs/environment.mdx +87 -0
- package/docs/faq.mdx +18 -16
- package/docs/first-drive.md +5 -13
- package/docs/flows.md +21 -2
- package/docs/frameworks.mdx +29 -10
- package/docs/getting-started.md +3 -3
- package/docs/http-transport.md +6 -3
- package/docs/human-control.md +2 -2
- package/docs/index.mdx +16 -6
- package/docs/install-agentic.mdx +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/integration-patterns.md +3 -3
- package/docs/multi-agent-testing.md +1 -1
- package/docs/packages/babel-plugin.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/react.mdx +2 -2
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +2 -2
- package/docs/packages.mdx +9 -9
- package/docs/predicates.mdx +50 -5
- package/docs/quickstart.mdx +2 -2
- package/docs/recipes.mdx +2 -2
- package/docs/state-management.mdx +2 -2
- package/docs/system-map.md +1 -1
- package/docs/telemetry.md +1 -1
- package/docs/token-efficiency.md +3 -3
- package/docs/tools/act-and-wait.mdx +1 -0
- package/docs/tools/act-sequence.mdx +1 -1
- package/docs/tools/console.mdx +1 -1
- package/docs/tools/inspect.mdx +1 -1
- package/docs/tools/network.mdx +1 -1
- package/docs/tools/query.mdx +1 -1
- package/docs/tools/sessions.mdx +1 -1
- package/docs/tools/snapshot.mdx +1 -1
- package/docs/tools/state.mdx +1 -1
- package/docs/tools/wait-for.mdx +1 -1
- package/docs/usage.md +7 -6
- package/docs/use-cases.mdx +157 -0
- package/docs/vs-screenshots.mdx +3 -3
- package/docs/what-is-recorded.md +2 -2
- package/docs/what-is-reticle.mdx +17 -20
- package/docs/why-reticle.mdx +14 -21
- package/package.json +10 -9
- package/dist/judgement/capsule/minimize.d.ts +0 -12
- package/dist/judgement/capsule/minimize.js +0 -22
- package/dist/judgement/phenomena/phenomena.d.ts +0 -27
- package/dist/judgement/phenomena/phenomena.js +0 -101
- package/dist/language/flows/flow-report.d.ts +0 -22
- package/dist/language/flows/flow-report.js +0 -106
|
@@ -91,6 +91,8 @@ export declare class Session implements HandshakeFacts {
|
|
|
91
91
|
* `reticle_session{action:"end"}` — a flag on the record, not on the page — could not help.
|
|
92
92
|
*/
|
|
93
93
|
unresponsive(): boolean;
|
|
94
|
+
/** False once the socket is closing or closed: dropping this record then orphans nothing. */
|
|
95
|
+
socketOpen(): boolean;
|
|
94
96
|
/** Wall-clock age of the session in milliseconds. */
|
|
95
97
|
staleMs(): number;
|
|
96
98
|
/** The document the most recent stamped event was observed under. See `#documentId`. */
|
|
@@ -114,6 +116,9 @@ export declare class Session implements HandshakeFacts {
|
|
|
114
116
|
* against the same control after a re-render gave it a different ref.
|
|
115
117
|
*/
|
|
116
118
|
private recordActedLabelFrom;
|
|
119
|
+
/** A control whose action PROVED its declared consequence — the coverage ledger's `proved`. */
|
|
120
|
+
recordProvedFrom(actPayload: unknown): void;
|
|
121
|
+
provedLabels(): ReadonlySet<string>;
|
|
117
122
|
actedRefs(): ReadonlySet<string>;
|
|
118
123
|
noteRateLimited(dropped: number): void;
|
|
119
124
|
blindSpots(): Readonly<Record<string, number>>;
|
|
@@ -16,7 +16,7 @@ import { isKnownRealm, EventType, HumanControlDataSchema, HumanControlKind, Huma
|
|
|
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
|
-
import { ObservedState } from './facts/observed-state.js';
|
|
19
|
+
import { ObservedState, controlLabelsOf } from './facts/observed-state.js';
|
|
20
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';
|
|
@@ -241,6 +241,10 @@ export class Session {
|
|
|
241
241
|
unresponsive() {
|
|
242
242
|
return this.#unansweredCommands >= UNRESPONSIVE_AFTER_TIMEOUTS;
|
|
243
243
|
}
|
|
244
|
+
/** False once the socket is closing or closed: dropping this record then orphans nothing. */
|
|
245
|
+
socketOpen() {
|
|
246
|
+
return this.#socket.readyState === WS_OPEN;
|
|
247
|
+
}
|
|
244
248
|
/** Wall-clock age of the session in milliseconds. */
|
|
245
249
|
staleMs() {
|
|
246
250
|
return this.#clock() - this.#startedAt;
|
|
@@ -352,24 +356,16 @@ export class Session {
|
|
|
352
356
|
* against the same control after a re-render gave it a different ref.
|
|
353
357
|
*/
|
|
354
358
|
recordActedLabelFrom(result) {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
this.#observed.recordActedLabel(testid);
|
|
366
|
-
const role = record['role'];
|
|
367
|
-
const name = record['name'];
|
|
368
|
-
if (typeof role !== 'string' || typeof name !== 'string')
|
|
369
|
-
return;
|
|
370
|
-
if (0 === role.length || 0 === name.length)
|
|
371
|
-
return;
|
|
372
|
-
this.#observed.recordActedLabel(`${role} "${name}"`);
|
|
359
|
+
for (const label of controlLabelsOf(result.result))
|
|
360
|
+
this.#observed.recordActedLabel(label);
|
|
361
|
+
}
|
|
362
|
+
/** A control whose action PROVED its declared consequence — the coverage ledger's `proved`. */
|
|
363
|
+
recordProvedFrom(actPayload) {
|
|
364
|
+
for (const label of controlLabelsOf(actPayload))
|
|
365
|
+
this.#observed.recordProvedLabel(label);
|
|
366
|
+
}
|
|
367
|
+
provedLabels() {
|
|
368
|
+
return this.#observed.provedLabels();
|
|
373
369
|
}
|
|
374
370
|
actedRefs() {
|
|
375
371
|
return this.#observed.actedRefs();
|
|
@@ -10,7 +10,8 @@ export interface SharedServer {
|
|
|
10
10
|
*/
|
|
11
11
|
attachMcp(factory: () => McpServer): void;
|
|
12
12
|
/** Register the JSON the daemon returns from GET /status (live sessions + health for `reticle status`). */
|
|
13
|
-
|
|
13
|
+
/** `held`: the request carried STATUS_HOLD_QUERY — a setup run is waiting on this daemon. */
|
|
14
|
+
attachStatus(provider: (held: boolean) => unknown): void;
|
|
14
15
|
/**
|
|
15
16
|
* Register the handler behind POST /drive: open a driveable browser at `url` and answer with the
|
|
16
17
|
* session it registered.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as http from 'node:http';
|
|
2
2
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
3
|
-
import { MCP_SSE_PATH, MCP_MESSAGE_PATH, MCP_SHUTDOWN_EVENT, STATUS_PATH, DRIVE_PATH, } from '@reticlehq/core';
|
|
3
|
+
import { MCP_SSE_PATH, MCP_MESSAGE_PATH, MCP_SHUTDOWN_EVENT, STATUS_PATH, STATUS_HOLD_QUERY, DRIVE_PATH, } from '@reticlehq/core';
|
|
4
4
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
5
5
|
import { noteAgentPeer, PEER_VERSION_PARAM, PEER_CONTRACT_PARAM, } from '../command/version/peer-announce.js';
|
|
6
6
|
import { log } from '../log.js';
|
|
@@ -76,7 +76,9 @@ export function createSharedServer(options = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
if ('GET' === req.method && path === STATUS_PATH) {
|
|
79
|
-
|
|
79
|
+
// A setup run waiting on this daemon says so on its polls; see daemon-usefulness.
|
|
80
|
+
const held = url.searchParams.has(STATUS_HOLD_QUERY);
|
|
81
|
+
const body = JSON.stringify(statusProvider?.(held) ?? { running: true });
|
|
80
82
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
81
83
|
res.end(body);
|
|
82
84
|
return;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export { SseFrameParser, type SseFrame } from './sse-frame-parser.js';
|
|
2
|
-
export { probeDaemon, waitForDaemon } from './proxy/proxy-daemon-probe.js';
|
|
2
|
+
export { probeDaemon, waitForDaemon, waitForDaemonBind } from './proxy/proxy-daemon-probe.js';
|
|
3
3
|
/**
|
|
4
4
|
* Re-exported so every existing caller (and every spec) keeps importing the proxy's log from the
|
|
5
5
|
* proxy. Where the code lives is a file-size decision; where it is imported from is an API.
|
|
6
6
|
*/
|
|
7
7
|
export { accountProxyLogWrite, proxyLog, proxyLogPath, PROXY_LOG_CHECK_BYTES, recoverOversizedProxyLog, setProxyLogPort, } from './proxy/proxy-log.js';
|
|
8
|
-
export { reconnectDelayMs
|
|
8
|
+
export { reconnectDelayMs } from './proxy/proxy-backoff.js';
|
|
9
9
|
export { MCP_PROXY_HTTP_AGENT_OPTIONS, postToSession, shouldRetryUnsentPost, type PostFailure, } from './proxy/mcp-post-transport.js';
|
|
10
10
|
/**
|
|
11
11
|
* Overridable so the survival spec can reach exhaustion in seconds instead of minutes — the same
|
|
@@ -6,7 +6,7 @@ import { toolsChangedNotification } from './proxy/proxy-handshake.js';
|
|
|
6
6
|
import { LOOPBACK_HOST, MCP_SSE_PATH, MCP_SHUTDOWN_EVENT, ReticleEnv, CONTRACT_FINGERPRINT, } from '@reticlehq/core';
|
|
7
7
|
import { SseFrameParser } from './sse-frame-parser.js';
|
|
8
8
|
export { SseFrameParser } from './sse-frame-parser.js';
|
|
9
|
-
export { probeDaemon, waitForDaemon } from './proxy/proxy-daemon-probe.js';
|
|
9
|
+
export { probeDaemon, waitForDaemon, waitForDaemonBind } 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';
|
|
@@ -33,7 +33,7 @@ import { OutageReason, OutageStage, reportMcpOutage } from './faults/mcp-outage.
|
|
|
33
33
|
import { postToSession } from './proxy/mcp-post-transport.js';
|
|
34
34
|
import { reconnectDelayMs } from './proxy/proxy-backoff.js';
|
|
35
35
|
import { PROXY_IDLE_EXIT_EVENT, ProxyIdleExit, resolveProxyIdleExitMs } from './proxy-idle-exit.js';
|
|
36
|
-
export { reconnectDelayMs
|
|
36
|
+
export { reconnectDelayMs } from './proxy/proxy-backoff.js';
|
|
37
37
|
export { MCP_PROXY_HTTP_AGENT_OPTIONS, postToSession, shouldRetryUnsentPost, } from './proxy/mcp-post-transport.js';
|
|
38
38
|
/**
|
|
39
39
|
* How many consecutive failed reconnects before the proxy stops RETRYING. It does not stop serving:
|
package/dist/surface/mcp/mcp.js
CHANGED
|
@@ -103,7 +103,7 @@ import { firstSentence } from '../tools/first-sentence.js';
|
|
|
103
103
|
* making it a pointer costs nothing.
|
|
104
104
|
*/
|
|
105
105
|
const PREDICATE_KINDS = 'Predicate object: { kind, ...fields }. kind is one of element | text | net | route | console | ' +
|
|
106
|
-
'animation | signal | state | settled | allOf | anyOf | not.';
|
|
106
|
+
'animation | signal | state | settled | compare | allOf | anyOf | not.';
|
|
107
107
|
/**
|
|
108
108
|
* Said once per turn: the bug-catching options, and where to get the rest of the field grammar.
|
|
109
109
|
*
|
|
@@ -118,9 +118,11 @@ const PREDICATE_KINDS = 'Predicate object: { kind, ...fields }. kind is one of e
|
|
|
118
118
|
* paying for six.
|
|
119
119
|
*/
|
|
120
120
|
const PREDICATE_FIELD_GRAMMAR_HINT = ' Bug-catching options: net.count (exact request count — catches double-submit), ' +
|
|
121
|
-
'net.bodyMatches (a
|
|
121
|
+
'net.bodyMatches (a JSON field match on what the SERVER answered — say the FIELD (a dotted path reaches nested ones) and its value, ' +
|
|
122
122
|
'because a substring of the body matches key names too), net.requestBodyMatches (the same on what ' +
|
|
123
123
|
'the UI SENT — the verdict for "applying this filter actually puts it in the payload"), ' +
|
|
124
|
+
'compare (page text vs server answer, no expected value: { left:{from:"text",scope}, ' +
|
|
125
|
+
'right:{from:"net",urlContains,path}, as:"number" }), ' +
|
|
124
126
|
'console.absent:true (action completed with a CLEAN ' +
|
|
125
127
|
'console), absent:true on element/text (it should be gone). Call reticle_tools for the full field ' +
|
|
126
128
|
'grammar of a kind.';
|
|
@@ -3,8 +3,12 @@ export declare function daemonPollDelayMs(attempt: number): number;
|
|
|
3
3
|
* Returns true if something is already listening on the reticle port.
|
|
4
4
|
* Uses a plain TCP probe so we don't create a side-effectful SSE session
|
|
5
5
|
* inside the daemon just to check reachability.
|
|
6
|
+
*
|
|
7
|
+
* `host` defaults to loopback, which is every caller's actual target — except `bootSession`
|
|
8
|
+
* (reticlehq/reticle#1165 review), whose Bridge can bind a non-default host via `RETICLE_HOST`.
|
|
9
|
+
* Hardcoding loopback here made that preflight check the wrong address.
|
|
6
10
|
*/
|
|
7
|
-
export declare function probeDaemon(port: number): Promise<boolean>;
|
|
11
|
+
export declare function probeDaemon(port: number, host?: string): Promise<boolean>;
|
|
8
12
|
/**
|
|
9
13
|
* Poll until a Reticle daemon is SERVING on `port`, or the deadline passes.
|
|
10
14
|
*
|
|
@@ -20,3 +24,11 @@ export declare function waitForDaemon(port: number, probes?: {
|
|
|
20
24
|
tcpOpen: (port: number) => Promise<boolean>;
|
|
21
25
|
status: (port: number) => Promise<unknown>;
|
|
22
26
|
}): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Poll until `/status` on `port` answers as a Reticle daemon; false once the bound passes.
|
|
29
|
+
*
|
|
30
|
+
* One rule for the three commands that start a daemon. `init` used to borrow `waitForDaemon`'s
|
|
31
|
+
* shorter reconnect budget, and on a cold fresh HOME it printed "could not start the Reticle
|
|
32
|
+
* daemon" over a daemon that bound seconds later. Bounded polling, never a fixed sleep.
|
|
33
|
+
*/
|
|
34
|
+
export declare function waitForDaemonBind(port: number): Promise<boolean>;
|
|
@@ -24,8 +24,12 @@ export function daemonPollDelayMs(attempt) {
|
|
|
24
24
|
* Returns true if something is already listening on the reticle port.
|
|
25
25
|
* Uses a plain TCP probe so we don't create a side-effectful SSE session
|
|
26
26
|
* inside the daemon just to check reachability.
|
|
27
|
+
*
|
|
28
|
+
* `host` defaults to loopback, which is every caller's actual target — except `bootSession`
|
|
29
|
+
* (reticlehq/reticle#1165 review), whose Bridge can bind a non-default host via `RETICLE_HOST`.
|
|
30
|
+
* Hardcoding loopback here made that preflight check the wrong address.
|
|
27
31
|
*/
|
|
28
|
-
export function probeDaemon(port) {
|
|
32
|
+
export function probeDaemon(port, host = LOOPBACK_HOST) {
|
|
29
33
|
return new Promise((resolve) => {
|
|
30
34
|
const socket = new net.Socket();
|
|
31
35
|
socket.setTimeout(500);
|
|
@@ -38,7 +42,7 @@ export function probeDaemon(port) {
|
|
|
38
42
|
socket.destroy();
|
|
39
43
|
resolve(false);
|
|
40
44
|
});
|
|
41
|
-
socket.connect(port,
|
|
45
|
+
socket.connect(port, host);
|
|
42
46
|
});
|
|
43
47
|
}
|
|
44
48
|
/**
|
|
@@ -69,3 +73,24 @@ export async function waitForDaemon(port, probes = { tcpOpen: probeDaemon, statu
|
|
|
69
73
|
throw new Error(`reticle daemon did not become ready on port ${String(port)} within ` +
|
|
70
74
|
`${String(DAEMON_READY_TIMEOUT_MS)}ms — ${describePresence(presence, port)}`);
|
|
71
75
|
}
|
|
76
|
+
/** How long a spawned daemon gets to bind before `serve`, `restart` or `init` gives up on it. */
|
|
77
|
+
const BIND_TIMEOUT_MS = 15_000;
|
|
78
|
+
const BIND_POLL_MS = 150;
|
|
79
|
+
/**
|
|
80
|
+
* Poll until `/status` on `port` answers as a Reticle daemon; false once the bound passes.
|
|
81
|
+
*
|
|
82
|
+
* One rule for the three commands that start a daemon. `init` used to borrow `waitForDaemon`'s
|
|
83
|
+
* shorter reconnect budget, and on a cold fresh HOME it printed "could not start the Reticle
|
|
84
|
+
* daemon" over a daemon that bound seconds later. Bounded polling, never a fixed sleep.
|
|
85
|
+
*/
|
|
86
|
+
export async function waitForDaemonBind(port) {
|
|
87
|
+
const deadline = Date.now() + BIND_TIMEOUT_MS;
|
|
88
|
+
for (;;) {
|
|
89
|
+
const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
90
|
+
if (presence === PortPresence.DAEMON)
|
|
91
|
+
return true;
|
|
92
|
+
if (Date.now() >= deadline)
|
|
93
|
+
return false;
|
|
94
|
+
await delay(BIND_POLL_MS);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -44,9 +44,9 @@ import { surfaceVocabulary, listOf } from './surface-vocabulary.js';
|
|
|
44
44
|
*/
|
|
45
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.
|
|
46
46
|
|
|
47
|
-
Only ${v.actAndWait}
|
|
47
|
+
Only ${v.act.length > 0 ? `${v.actAndWait}, ${v.assert} and ${v.act}{steps}` : `${v.actAndWait} and ${v.assert}`} produce a verdict. Everything 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.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Done is the journey's END proved, not its first step: drive each step your change touches and give the last an \`until\` naming the end state (\`durable: true\` if it must persist). A "yes" on it over a clean capture IS the answer — stop.
|
|
50
50
|
|
|
51
51
|
DIAGNOSING a bug? Read the source first — Reticle proves what the app DOES, not why. Drive to CONFIRM a fix, not to find one.`;
|
|
52
52
|
/**
|
|
@@ -84,13 +84,15 @@ const replayFirst = (v) => {
|
|
|
84
84
|
*
|
|
85
85
|
* Deliberately concrete: the command, what it does to the app, and the call that confirms it. A
|
|
86
86
|
* general instruction to "set Reticle up" is the state these agents are already in — they have the
|
|
87
|
-
* tools and no app, and nothing tells them that combination is incomplete.
|
|
88
|
-
* server
|
|
89
|
-
*
|
|
87
|
+
* tools and no app, and nothing tells them that combination is incomplete. `init` starts the dev
|
|
88
|
+
* server and opens the app itself, so the restart is named only for the case it cannot cover: a dev
|
|
89
|
+
* server that was already running read the config before `init` edited it, which is the single most
|
|
90
|
+
* common way this step half-succeeds. Telling every agent to restart sent them to kill the server
|
|
91
|
+
* `init` had just started.
|
|
90
92
|
*/
|
|
91
93
|
const firstMoveFor = (v) => `FIRST: no app has ever connected to Reticle in this project, so nothing here can be verified yet. Having these tools is not the same as being set up — the tools reach a daemon, and the daemon has nothing to look at until the app itself is instrumented.
|
|
92
94
|
|
|
93
|
-
Fix that before anything else: run \`npx @reticlehq/server init\` in the project
|
|
95
|
+
Fix that before anything else: run \`npx @reticlehq/server init\` in the project. It starts the dev server and opens the app itself; only if a dev server was ALREADY running without Reticle, restart it and reload the tab. Confirm with ${v.sessions} — a session listed there is the proof, and until one appears no other tool can tell you anything about this app.`;
|
|
94
96
|
/**
|
|
95
97
|
* When to reach for the tools that are advertised but never explained, and how to reach the two
|
|
96
98
|
* that are explained but not advertised. Both halves of the same defect.
|
|
@@ -111,7 +113,7 @@ Fix that before anything else: run \`npx @reticlehq/server init\` in the project
|
|
|
111
113
|
*/
|
|
112
114
|
const reachFor = (v) => {
|
|
113
115
|
const lines = [
|
|
114
|
-
`${v.observe} is the evidence channel: everything the page did since a cursor, in one read. Take it BEFORE you call a build broken — measured, stripping the observation tools TRIPLED false alarms
|
|
116
|
+
`${v.observe} is the evidence channel: everything the page did since a cursor, in one read. Take it BEFORE you call a build broken — measured, stripping the observation tools TRIPLED false alarms. ${v.settle} settles a page that is changing without you; ${v.inspect} maps an element to its source file:line, which turns a finding into an edit${v.yield.length > 0 ? `; ${v.yield} hands the tab back to the human` : ''}.`,
|
|
115
117
|
];
|
|
116
118
|
// Said ONLY when both halves are advertised. A catalogue with no way to invoke what it lists sends
|
|
117
119
|
// the agent at a tool it cannot call and then off the product entirely — see
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What act_and_wait does between the moment its predicate first holds and the moment it grades.
|
|
3
|
+
*
|
|
4
|
+
* The predicate resolves the INSTANT it holds, which on an optimistically-navigating app is while
|
|
5
|
+
* the write is still in flight — so the verdict was taken over a window the app had not finished,
|
|
6
|
+
* and came back `unknown / unsettled` asking the CALLER to re-check. A login that genuinely
|
|
7
|
+
* succeeded returned at 492ms of an 8000ms budget with every corroborating channel agreeing. So the
|
|
8
|
+
* rest of the budget the caller already granted is spent here, in three steps, each deciding
|
|
9
|
+
* nothing on its own.
|
|
10
|
+
*/
|
|
11
|
+
import type { Predicate } from '@reticlehq/core';
|
|
12
|
+
import { waitForInFlight } from './settle-in-flight.js';
|
|
13
|
+
import { revertedAfterMatch } from './durable.js';
|
|
14
|
+
type Waited = Parameters<typeof waitForInFlight>[0] & Parameters<typeof revertedAfterMatch>[0];
|
|
15
|
+
/** Returns the sentence to fail with when the consequence reverted, else undefined. */
|
|
16
|
+
export declare function finishAfterMatch(session: Waited, until: Predicate, since: number, baselines: Parameters<typeof revertedAfterMatch>[3], remaining: () => number): Promise<string | undefined>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What act_and_wait does between the moment its predicate first holds and the moment it grades.
|
|
3
|
+
*
|
|
4
|
+
* The predicate resolves the INSTANT it holds, which on an optimistically-navigating app is while
|
|
5
|
+
* the write is still in flight — so the verdict was taken over a window the app had not finished,
|
|
6
|
+
* and came back `unknown / unsettled` asking the CALLER to re-check. A login that genuinely
|
|
7
|
+
* succeeded returned at 492ms of an 8000ms budget with every corroborating channel agreeing. So the
|
|
8
|
+
* rest of the budget the caller already granted is spent here, in three steps, each deciding
|
|
9
|
+
* nothing on its own.
|
|
10
|
+
*/
|
|
11
|
+
import { waitForInFlight } from './settle-in-flight.js';
|
|
12
|
+
import { waitForReaction } from './react-grace.js';
|
|
13
|
+
import { revertedAfterMatch } from './durable.js';
|
|
14
|
+
/** Returns the sentence to fail with when the consequence reverted, else undefined. */
|
|
15
|
+
export async function finishAfterMatch(session, until, since, baselines, remaining) {
|
|
16
|
+
const sleep = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) };
|
|
17
|
+
// 1. Follow any open request to its response. A response that arrives as a FAILURE still
|
|
18
|
+
// contradicts — and now arrives INSIDE the window, where the detector can see it at all — while a
|
|
19
|
+
// request still open when the budget runs out reports unsettled, an honest limit rather than an
|
|
20
|
+
// early exit. Costs nothing on the common path, where no request is in flight.
|
|
21
|
+
await waitForInFlight(session, since, remaining(), sleep);
|
|
22
|
+
// 2. Then the app's REACTION to that response. Close the window between the response and the
|
|
23
|
+
// re-render and every channel agrees the app took a successful write and did nothing —
|
|
24
|
+
// `response-ignored`, a `verified:"no"` on a correct app, produced entirely by where we stopped
|
|
25
|
+
// looking. Paid only in the shape that would otherwise be accused, and short enough that a
|
|
26
|
+
// genuinely dropped response is still reported.
|
|
27
|
+
await waitForReaction(session, since, remaining(), sleep);
|
|
28
|
+
// 3. Then ask whether what held still holds — see revertedAfterMatch.
|
|
29
|
+
return revertedAfterMatch(session, until, since, baselines);
|
|
30
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `act_and_wait { durable: true }` — after a yes, reload and ask again.
|
|
3
|
+
*
|
|
4
|
+
* Every verdict is scoped to one action's window, so "saved" was proved by the toast that said so,
|
|
5
|
+
* and nothing ever asked whether the thing was still there once the page came back. That is the
|
|
6
|
+
* difference between a UI that advanced and a change that persisted, and it was reachable only by
|
|
7
|
+
* hand: navigate, then a separate assert that nothing tied to the action that caused it.
|
|
8
|
+
*
|
|
9
|
+
* Only the part of `until` that describes the page as it IS can be asked of a fresh document. A
|
|
10
|
+
* request or a signal happened once, in a window that died with the old document; a route is read
|
|
11
|
+
* from route events a reload does not replay. So those clauses are dropped, never re-asserted.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { type Predicate } from '@reticlehq/core';
|
|
15
|
+
import type { decideVerified } from '@reticlehq/engine/evidence/verified.js';
|
|
16
|
+
type VerifiedVerdict = ReturnType<typeof decideVerified>;
|
|
17
|
+
import { evaluatePredicate } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
18
|
+
import type { Session } from '../../../portal/session/session.js';
|
|
19
|
+
export declare const durableArg: z.ZodOptional<z.ZodBoolean>;
|
|
20
|
+
export declare const durableOutput: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
21
|
+
/** The part of a predicate a fresh document can answer, or undefined when there is none. */
|
|
22
|
+
export declare function durablePart(p: Predicate): Predicate | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* What the re-check found. `held` absent means no answer: the page never came back to be asked, or
|
|
25
|
+
* it came back and could not answer (`inconclusive`).
|
|
26
|
+
*/
|
|
27
|
+
export interface Durability {
|
|
28
|
+
held?: boolean;
|
|
29
|
+
observed?: string;
|
|
30
|
+
skipped?: string;
|
|
31
|
+
inconclusive?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A re-check's predicate result, as durability. Only a page that ANSWERED can say the consequence is
|
|
35
|
+
* gone: an unreadable one (its store not up yet) or one the tab left mid-read is no answer, and
|
|
36
|
+
* reporting either as "did not persist" is a confident no about something nobody saw.
|
|
37
|
+
*/
|
|
38
|
+
export declare function durabilityOf(verdict: {
|
|
39
|
+
pass: boolean;
|
|
40
|
+
observed?: string | undefined;
|
|
41
|
+
inconclusive?: string | undefined;
|
|
42
|
+
observationLost?: boolean | undefined;
|
|
43
|
+
}): Durability;
|
|
44
|
+
/**
|
|
45
|
+
* Apply `durable` to a decided verdict. Pure over `recheck`, which the caller supplies — see
|
|
46
|
+
* reloadAndRecheck for the one that reloads a real page.
|
|
47
|
+
*/
|
|
48
|
+
export declare function withDurability(asked: unknown, until: Predicate, decision: VerifiedVerdict, recheck: (part: Predicate) => Promise<Durability>): Promise<{
|
|
49
|
+
decision: VerifiedVerdict;
|
|
50
|
+
durable?: Durability;
|
|
51
|
+
}>;
|
|
52
|
+
/** Reload the page, wait for it to re-announce itself, and ask `part` of the fresh document. */
|
|
53
|
+
export declare function reloadAndRecheck(deps: {
|
|
54
|
+
sessions: {
|
|
55
|
+
get(id: string): Session | undefined;
|
|
56
|
+
};
|
|
57
|
+
now: () => number;
|
|
58
|
+
}, session: Session, part: Predicate, timeoutMs: number): Promise<Durability>;
|
|
59
|
+
/**
|
|
60
|
+
* Whether the page-state part of a consequence that held has since gone — an optimistic UI that
|
|
61
|
+
* rolled back. Returns the sentence to fail with, or undefined when it still holds.
|
|
62
|
+
*
|
|
63
|
+
* The wait resolves the instant a predicate holds, and the in-flight wait after it only follows a
|
|
64
|
+
* request to its response. A row that appeared and was removed a tick later, with no failing
|
|
65
|
+
* request to contradict it, was a yes. Only element/text/state clauses are re-read: a request or a
|
|
66
|
+
* signal does not un-happen, and re-reading `settled` on a page that polls would accuse it falsely.
|
|
67
|
+
*/
|
|
68
|
+
export declare function revertedAfterMatch(session: Parameters<typeof evaluatePredicate>[0], until: Predicate, since: number, baselines: Parameters<typeof evaluatePredicate>[4]): Promise<string | undefined>;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `act_and_wait { durable: true }` — after a yes, reload and ask again.
|
|
3
|
+
*
|
|
4
|
+
* Every verdict is scoped to one action's window, so "saved" was proved by the toast that said so,
|
|
5
|
+
* and nothing ever asked whether the thing was still there once the page came back. That is the
|
|
6
|
+
* difference between a UI that advanced and a change that persisted, and it was reachable only by
|
|
7
|
+
* hand: navigate, then a separate assert that nothing tied to the action that caused it.
|
|
8
|
+
*
|
|
9
|
+
* Only the part of `until` that describes the page as it IS can be asked of a fresh document. A
|
|
10
|
+
* request or a signal happened once, in a window that died with the old document; a route is read
|
|
11
|
+
* from route events a reload does not replay. So those clauses are dropped, never re-asserted.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { PredicateKind, ReticleCommand, Verified, VerifiedReason, } from '@reticlehq/core';
|
|
15
|
+
import { evaluatePredicate, waitForPredicate, } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
16
|
+
import { RELOAD_RECONNECT_TIMEOUT_MS, waitForReconnect, } from '../../../portal/session/session-reconnect.js';
|
|
17
|
+
export const durableArg = z
|
|
18
|
+
.boolean()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('After a yes, reload and require the element/text/state part of `until` to hold again.');
|
|
21
|
+
// Loose and undescribed on purpose: the output surface is re-sent every turn. `because` carries
|
|
22
|
+
// the answer; this is the detail — { held?, observed?, skipped? }.
|
|
23
|
+
export const durableOutput = z.record(z.unknown()).optional();
|
|
24
|
+
const RE_READABLE = new Set([
|
|
25
|
+
PredicateKind.ELEMENT,
|
|
26
|
+
PredicateKind.TEXT,
|
|
27
|
+
PredicateKind.STATE,
|
|
28
|
+
]);
|
|
29
|
+
/** The part of a predicate a fresh document can answer, or undefined when there is none. */
|
|
30
|
+
export function durablePart(p) {
|
|
31
|
+
if (PredicateKind.ALL_OF === p.kind) {
|
|
32
|
+
const kept = p.predicates.map(durablePart).filter((c) => c !== undefined);
|
|
33
|
+
if (0 === kept.length)
|
|
34
|
+
return undefined;
|
|
35
|
+
return 1 === kept.length ? kept[0] : { kind: PredicateKind.ALL_OF, predicates: kept };
|
|
36
|
+
}
|
|
37
|
+
// Narrowing an OR to the branches that survive would change the claim, not re-check it.
|
|
38
|
+
if (PredicateKind.ANY_OF === p.kind)
|
|
39
|
+
return p.predicates.every((c) => durablePart(c) === c) ? p : undefined;
|
|
40
|
+
if (PredicateKind.NOT === p.kind)
|
|
41
|
+
return durablePart(p.predicate) === p.predicate ? p : undefined;
|
|
42
|
+
return RE_READABLE.has(p.kind) ? p : undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A re-check's predicate result, as durability. Only a page that ANSWERED can say the consequence is
|
|
46
|
+
* gone: an unreadable one (its store not up yet) or one the tab left mid-read is no answer, and
|
|
47
|
+
* reporting either as "did not persist" is a confident no about something nobody saw.
|
|
48
|
+
*/
|
|
49
|
+
export function durabilityOf(verdict) {
|
|
50
|
+
const observed = verdict.observed === undefined ? {} : { observed: verdict.observed };
|
|
51
|
+
if (true === verdict.pass)
|
|
52
|
+
return { held: true, ...observed };
|
|
53
|
+
if (verdict.inconclusive !== undefined)
|
|
54
|
+
return { inconclusive: verdict.inconclusive, ...observed };
|
|
55
|
+
if (true === verdict.observationLost)
|
|
56
|
+
return observed;
|
|
57
|
+
return { held: false, ...observed };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Apply `durable` to a decided verdict. Pure over `recheck`, which the caller supplies — see
|
|
61
|
+
* reloadAndRecheck for the one that reloads a real page.
|
|
62
|
+
*/
|
|
63
|
+
export async function withDurability(asked, until, decision, recheck) {
|
|
64
|
+
if (true !== asked || Verified.YES !== decision.verified)
|
|
65
|
+
return { decision };
|
|
66
|
+
const part = durablePart(until);
|
|
67
|
+
if (part === undefined) {
|
|
68
|
+
const skipped = 'nothing declared describes the page itself — a request or a signal happens once and cannot be seen again after a reload; add an element, text or state clause';
|
|
69
|
+
return { decision, durable: { skipped } };
|
|
70
|
+
}
|
|
71
|
+
const durable = await recheck(part);
|
|
72
|
+
if (true === durable.held)
|
|
73
|
+
return { decision, durable };
|
|
74
|
+
if (durable.inconclusive !== undefined) {
|
|
75
|
+
return {
|
|
76
|
+
durable,
|
|
77
|
+
decision: {
|
|
78
|
+
verified: Verified.UNKNOWN,
|
|
79
|
+
verifiedReason: VerifiedReason.INCONCLUSIVE,
|
|
80
|
+
because: `the consequence held, but the reloaded page could not answer whether it still does (${durable.inconclusive})`,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (durable.held === undefined) {
|
|
85
|
+
return {
|
|
86
|
+
durable,
|
|
87
|
+
decision: {
|
|
88
|
+
verified: Verified.UNKNOWN,
|
|
89
|
+
verifiedReason: VerifiedReason.OBSERVATION_LOST,
|
|
90
|
+
because: `the consequence held, but the page did not come back after the reload to be asked again${durable.observed === undefined ? '' : ` (${durable.observed})`}`,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
durable,
|
|
96
|
+
decision: {
|
|
97
|
+
verified: Verified.NO,
|
|
98
|
+
verifiedReason: VerifiedReason.ASSERTION_FAILED,
|
|
99
|
+
because: `the consequence held, then was gone after a reload — it did not persist${durable.observed === undefined ? '' : ` (observed ${durable.observed})`}`,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Reload the page, wait for it to re-announce itself, and ask `part` of the fresh document. */
|
|
104
|
+
export async function reloadAndRecheck(deps, session, part, timeoutMs) {
|
|
105
|
+
// Same floor navigate { reload } sets: nothing from the old document may answer for the new one.
|
|
106
|
+
session.lastAct.markNavigated(session.elapsed());
|
|
107
|
+
const sent = await session.command(ReticleCommand.REFRESH, { hard: false });
|
|
108
|
+
if (!sent.ok)
|
|
109
|
+
return { observed: sent.error ?? 'the reload was refused' };
|
|
110
|
+
const back = await waitForReconnect({
|
|
111
|
+
current: () => deps.sessions.get(session.id),
|
|
112
|
+
previous: session,
|
|
113
|
+
timeoutMs: Math.max(timeoutMs, RELOAD_RECONNECT_TIMEOUT_MS),
|
|
114
|
+
now: deps.now,
|
|
115
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
116
|
+
});
|
|
117
|
+
const fresh = back ? deps.sessions.get(session.id) : undefined;
|
|
118
|
+
if (fresh === undefined)
|
|
119
|
+
return {};
|
|
120
|
+
return durabilityOf(await waitForPredicate(fresh, part, timeoutMs, 0));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Whether the page-state part of a consequence that held has since gone — an optimistic UI that
|
|
124
|
+
* rolled back. Returns the sentence to fail with, or undefined when it still holds.
|
|
125
|
+
*
|
|
126
|
+
* The wait resolves the instant a predicate holds, and the in-flight wait after it only follows a
|
|
127
|
+
* request to its response. A row that appeared and was removed a tick later, with no failing
|
|
128
|
+
* request to contradict it, was a yes. Only element/text/state clauses are re-read: a request or a
|
|
129
|
+
* signal does not un-happen, and re-reading `settled` on a page that polls would accuse it falsely.
|
|
130
|
+
*/
|
|
131
|
+
export async function revertedAfterMatch(session, until, since, baselines) {
|
|
132
|
+
const part = durablePart(until);
|
|
133
|
+
if (part === undefined)
|
|
134
|
+
return undefined;
|
|
135
|
+
const now = await evaluatePredicate(session, part, since, true, baselines);
|
|
136
|
+
if (true === now.pass || now.inconclusive !== undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
return `held, then reverted before the action finished${now.observed === undefined ? '' : ` — now ${now.observed}`}`;
|
|
139
|
+
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* would make the daemon wait. `assert` / `wait_for` / `act_and_wait` all spend the caller's
|
|
18
18
|
* `timeout_ms` (resolve-within.ts); this is the same budget applied to the same wait.
|
|
19
19
|
*/
|
|
20
|
+
import type { Session } from '../../../../portal/session/session.js';
|
|
20
21
|
import type { SessionManager } from '../../../../portal/session/session-manager.js';
|
|
21
22
|
import type { NavigateArrival } from './navigate-result.js';
|
|
22
23
|
/**
|
|
@@ -45,14 +46,20 @@ export declare function absoluteTarget(target: string, base: string): string;
|
|
|
45
46
|
*/
|
|
46
47
|
export interface ArrivalScope {
|
|
47
48
|
/**
|
|
48
|
-
* The
|
|
49
|
+
* The actual Session object that received the navigation command.
|
|
49
50
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* report `confirmed: false` for the navigations most likely to have worked.
|
|
51
|
+
* A full-document navigation can destroy this session and reconnect the new document
|
|
52
|
+
* with the same id or a different id, so successor detection needs the original object.
|
|
53
53
|
*/
|
|
54
|
-
|
|
55
|
-
/**
|
|
54
|
+
navigatedSession: Session;
|
|
55
|
+
/**
|
|
56
|
+
* The URL the session was on before navigation started.
|
|
57
|
+
*
|
|
58
|
+
* This lets us distinguish "the navigation is still in progress" from
|
|
59
|
+
* "the app redirected the newly arrived document somewhere else."
|
|
60
|
+
*/
|
|
61
|
+
navigatedFrom: string;
|
|
62
|
+
/** Sessions already on the target before dispatch cannot prove this navigation arrived. */
|
|
56
63
|
priorIds: ReadonlySet<string>;
|
|
57
64
|
}
|
|
58
65
|
/**
|