@reticlehq/server 3.4.0 → 3.6.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/README.md +1 -1
- package/SKILL.md +16 -8
- package/dist/chromium-launch-options.d.ts +3 -1
- package/dist/chromium-launch-options.js +2 -1
- package/dist/command/cli/adhoc-verdict.d.ts +1 -0
- package/dist/command/cli/adhoc-verdict.js +6 -3
- package/dist/command/cli/answers/open-note.d.ts +2 -0
- package/dist/command/cli/answers/open-note.js +22 -0
- package/dist/command/cli/auth/cloud-session.d.ts +4 -4
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-flow-commands.js +4 -8
- package/dist/command/cli/cli-parse.d.ts +1 -1
- package/dist/command/cli/cli-parse.js +13 -11
- package/dist/command/cli/cli-verify.d.ts +1 -1
- package/dist/command/cli/cli-verify.js +35 -25
- package/dist/command/cli/cloud-cli.js +110 -12
- package/dist/command/cli/cloud-login.d.ts +9 -1
- package/dist/command/cli/cloud-login.js +43 -8
- package/dist/command/cli/demo/demo-page.js +11 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
- package/dist/command/cli/drive/drive-attach.d.ts +27 -2
- package/dist/command/cli/drive/drive-attach.js +71 -4
- package/dist/command/cli/drive/drive-command.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +13 -7
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
- package/dist/command/cli/ports/port-holder.d.ts +10 -0
- package/dist/command/cli/ports/port-holder.js +24 -0
- package/dist/command/cli/setup-mcp-cli.js +15 -5
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +6 -9
- package/dist/command/cli.d.ts +6 -0
- package/dist/command/cli.js +45 -23
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
- package/dist/command/daemon/daemon-resolve.d.ts +14 -0
- package/dist/command/daemon/daemon-resolve.js +33 -2
- package/dist/command/setup/agent-writer.d.ts +1 -1
- package/dist/command/setup/agent-writer.js +3 -2
- package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
- package/dist/command/setup/bringup/connect-lease.js +1 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +10 -0
- package/dist/command/setup/bringup/dev-server-wait.js +20 -1
- package/dist/command/setup/init/init-port.d.ts +4 -0
- package/dist/command/setup/init/init-port.js +10 -0
- package/dist/command/setup/init/init-runtime.d.ts +10 -0
- package/dist/command/setup/init/init-runtime.js +71 -9
- package/dist/command/setup/node-effects.js +8 -2
- package/dist/command/setup/remaining-steps.js +2 -1
- package/dist/command/setup/run-setup.d.ts +7 -0
- package/dist/command/setup/run-setup.js +38 -25
- package/dist/command/setup/setup-command.d.ts +2 -1
- package/dist/command/setup/setup-command.js +2 -0
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +14 -5
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- package/dist/command/version/version-skew.js +11 -2
- package/dist/features/crawl/crawl-tools.js +4 -0
- package/dist/features/crawl/crawl.d.ts +6 -0
- package/dist/features/crawl/crawl.js +9 -1
- package/dist/features/exhaust/ledger.d.ts +7 -0
- package/dist/features/exhaust/ledger.js +35 -9
- package/dist/features/harness/app-findings.d.ts +2 -0
- package/dist/features/harness/app-findings.js +52 -0
- package/dist/features/harness/drive-report.d.ts +27 -1
- package/dist/features/harness/drive-report.js +162 -11
- package/dist/features/harness/drivers.d.ts +10 -14
- package/dist/features/harness/drivers.js +10 -14
- package/dist/features/harness/fill-values.d.ts +3 -69
- package/dist/features/harness/fill-values.js +3 -133
- package/dist/features/harness/goals.d.ts +21 -0
- package/dist/features/harness/goals.js +77 -0
- package/dist/features/harness/harness.d.ts +21 -0
- package/dist/features/harness/harness.js +69 -2
- package/dist/features/harness/platform/drive-target.d.ts +30 -0
- package/dist/features/harness/platform/drive-target.js +81 -0
- package/dist/features/harness/platform/local-apps.d.ts +94 -0
- package/dist/features/harness/platform/local-apps.js +204 -0
- package/dist/features/harness/platform/remote-drive.d.ts +180 -0
- package/dist/features/harness/platform/remote-drive.js +365 -0
- package/dist/features/harness/platform/script.d.ts +48 -0
- package/dist/features/harness/platform/script.js +128 -0
- package/dist/features/harness/platform/server-driver.d.ts +41 -0
- package/dist/features/harness/platform/server-driver.js +179 -0
- package/dist/features/harness/platform-config.d.ts +7 -0
- package/dist/features/harness/platform-config.js +14 -2
- package/dist/features/harness/script/app-controls.d.ts +5 -0
- package/dist/features/harness/script/app-controls.js +40 -0
- package/dist/features/harness/script/lane-ids.d.ts +14 -0
- package/dist/features/harness/script/lane-ids.js +16 -0
- package/dist/features/harness/script/script-run.d.ts +41 -0
- package/dist/features/harness/script/script-run.js +299 -0
- package/dist/features/license/license.d.ts +2 -2
- package/dist/features/visual/visual-tools.d.ts +10 -1
- package/dist/features/visual/visual-tools.js +40 -3
- package/dist/hooks/driven-by.d.ts +4 -0
- package/dist/hooks/driven-by.js +16 -0
- package/dist/hooks/hook-commands.js +15 -4
- package/dist/hooks/tool-log.d.ts +9 -0
- package/dist/hooks/tool-log.js +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +91 -89
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
- package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
- package/dist/judgement/runs/artifact/run-store.js +26 -1
- package/dist/judgement/runs/drive-run.d.ts +22 -0
- package/dist/judgement/runs/drive-run.js +99 -1
- package/dist/judgement/runs/repair-prompt.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +8 -1
- package/dist/judgement/runs/verification-sync.d.ts +3 -2
- package/dist/judgement/runs/verification-sync.js +4 -3
- package/dist/language/flows/change/gate.d.ts +10 -0
- package/dist/language/flows/change/gate.js +22 -0
- package/dist/language/flows/decision.js +34 -0
- package/dist/language/flows/flow-anchor.d.ts +54 -8
- package/dist/language/flows/flow-anchor.js +88 -3
- package/dist/language/flows/flow-cross-step.d.ts +15 -0
- package/dist/language/flows/flow-cross-step.js +21 -0
- package/dist/language/flows/flow-learning.js +8 -11
- package/dist/language/flows/flow-mutate-tools.js +5 -3
- package/dist/language/flows/flow-replay-run.d.ts +23 -9
- package/dist/language/flows/flow-replay-run.js +101 -94
- package/dist/language/flows/flow-replay.d.ts +23 -1
- package/dist/language/flows/flow-replay.js +92 -19
- package/dist/language/flows/flow-scope.js +5 -1
- package/dist/language/flows/flow-step-runners.d.ts +20 -2
- package/dist/language/flows/flow-step-runners.js +236 -42
- package/dist/language/flows/flow-store-for-session.d.ts +2 -2
- package/dist/language/flows/flow-store-for-session.js +2 -2
- package/dist/language/flows/flow-tools.js +58 -34
- package/dist/language/flows/flows.js +5 -2
- package/dist/language/flows/heal-run.js +6 -4
- package/dist/language/flows/recording/flake.d.ts +2 -2
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
- package/dist/language/flows/recording/tape/recordings.js +40 -8
- package/dist/language/flows/replay-from-hud.d.ts +20 -0
- package/dist/language/flows/replay-from-hud.js +51 -0
- package/dist/language/flows/replay.d.ts +9 -5
- package/dist/language/flows/replay.js +27 -9
- package/dist/language/flows/suite/suite-flakes.js +2 -0
- package/dist/language/flows/suite-selection.d.ts +2 -15
- package/dist/language/flows/suite-selection.js +4 -3
- package/dist/language/flows/verify-change-tools.js +8 -4
- package/dist/launch-chromium.d.ts +27 -5
- package/dist/launch-chromium.js +38 -8
- package/dist/memory/cloud/cloud-config.d.ts +12 -0
- package/dist/memory/cloud/cloud-config.js +15 -0
- package/dist/memory/cloud/cloud-sync.d.ts +17 -14
- package/dist/memory/cloud/cloud-sync.js +12 -1
- package/dist/memory/cloud/harness-config.d.ts +26 -1
- package/dist/memory/cloud/harness-config.js +99 -4
- package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
- package/dist/memory/cloud/hud-notices-source.js +128 -0
- package/dist/memory/cloud/memory-scope.d.ts +144 -0
- package/dist/memory/cloud/memory-scope.js +205 -0
- package/dist/memory/cloud/sync-cycle.d.ts +21 -3
- package/dist/memory/cloud/sync-cycle.js +45 -5
- package/dist/memory/cloud/sync-daemon.js +7 -5
- package/dist/memory/cloud/sync-disk.d.ts +1 -1
- package/dist/memory/cloud/sync-disk.js +5 -8
- package/dist/memory/impact/account-watch.d.ts +19 -0
- package/dist/memory/impact/account-watch.js +34 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +12 -2
- package/dist/memory/impact/impact-store.d.ts +16 -0
- package/dist/memory/impact/impact-store.js +142 -26
- package/dist/memory/intent/intent-shard.d.ts +2 -2
- package/dist/memory/intent/intent-tools.js +88 -5
- package/dist/memory/journal/envelope.d.ts +16 -16
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- package/dist/memory/journal/journal-recorder.js +7 -1
- package/dist/memory/journal/on-disk/retention.d.ts +3 -0
- package/dist/memory/journal/on-disk/retention.js +5 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
- package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
- package/dist/memory/journal/session-end.js +6 -4
- package/dist/memory/project/artifact-root-resolver.d.ts +36 -1
- package/dist/memory/project/artifact-root-resolver.js +134 -26
- package/dist/memory/project/artifact-root.d.ts +42 -5
- package/dist/memory/project/artifact-root.js +87 -8
- package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
- package/dist/memory/project/dir/reticle-dir.js +3 -1
- package/dist/memory/project/project-tools.js +22 -3
- package/dist/memory/project/session-root.d.ts +24 -1
- package/dist/memory/project/session-root.js +41 -5
- package/dist/memory/project/sync-status.d.ts +39 -0
- package/dist/memory/project/sync-status.js +90 -0
- package/dist/memory/recall/memory-tools.js +2 -0
- package/dist/memory/recall/project-memory.d.ts +19 -0
- package/dist/memory/recall/project-memory.js +45 -6
- package/dist/portal/bridge/bridge.d.ts +7 -4
- package/dist/portal/bridge/bridge.js +25 -3
- package/dist/portal/bridge/pairing-token.d.ts +9 -0
- package/dist/portal/bridge/pairing-token.js +17 -0
- package/dist/portal/input/real-input.d.ts +81 -10
- package/dist/portal/input/real-input.js +207 -22
- package/dist/portal/input/viewport-tools.js +19 -9
- package/dist/portal/pool/browser-pool.d.ts +22 -9
- package/dist/portal/pool/browser-pool.js +70 -35
- package/dist/portal/pool/playwright-launcher.d.ts +13 -0
- package/dist/portal/pool/playwright-launcher.js +17 -1
- package/dist/portal/pool/pool-contract.d.ts +7 -4
- package/dist/portal/session/command-payload.d.ts +8 -0
- package/dist/portal/session/command-payload.js +19 -0
- package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
- package/dist/portal/session/dev-server/dev-command.js +61 -12
- package/dist/portal/session/facts/departure-note.d.ts +22 -0
- package/dist/portal/session/facts/departure-note.js +47 -0
- package/dist/portal/session/facts/session-replaced.d.ts +5 -0
- package/dist/portal/session/facts/session-replaced.js +5 -0
- package/dist/portal/session/first-run-wiring.d.ts +42 -0
- package/dist/portal/session/first-run-wiring.js +132 -0
- package/dist/portal/session/human/notes-ledger.d.ts +20 -0
- package/dist/portal/session/human/notes-ledger.js +55 -0
- package/dist/portal/session/human/review-store.d.ts +12 -0
- package/dist/portal/session/human/review-store.js +18 -10
- package/dist/portal/session/live-control-tools.d.ts +0 -13
- package/dist/portal/session/live-control-tools.js +28 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
- package/dist/portal/session/no-session-diagnosis.js +15 -0
- package/dist/portal/session/no-session-next-action.d.ts +11 -0
- package/dist/portal/session/no-session-next-action.js +31 -5
- package/dist/portal/session/no-session-watch.d.ts +14 -1
- package/dist/portal/session/no-session-watch.js +63 -10
- package/dist/portal/session/presence/session-mirror.js +3 -0
- package/dist/portal/session/serving-directory.d.ts +27 -0
- package/dist/portal/session/serving-directory.js +45 -0
- package/dist/portal/session/session-health.d.ts +7 -0
- package/dist/portal/session/session-health.js +6 -0
- package/dist/portal/session/session-info.d.ts +8 -1
- package/dist/portal/session/session-info.js +6 -0
- package/dist/portal/session/session-manager.d.ts +21 -0
- package/dist/portal/session/session-manager.js +44 -8
- package/dist/portal/session/session-reaper.js +4 -1
- package/dist/portal/session/session-successor.d.ts +5 -0
- package/dist/portal/session/session-tools.js +26 -3
- package/dist/portal/session/session.d.ts +12 -10
- package/dist/portal/session/session.js +30 -31
- package/dist/start-options.d.ts +6 -0
- package/dist/status-payload.d.ts +10 -0
- package/dist/status-payload.js +23 -0
- package/dist/surface/http-server.d.ts +1 -1
- package/dist/surface/http-server.js +15 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
- package/dist/surface/mcp/mcp-proxy.js +36 -5
- package/dist/surface/mcp/mcp.d.ts +11 -6
- package/dist/surface/mcp/mcp.js +28 -14
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- package/dist/surface/mcp/server-instructions.d.ts +4 -2
- package/dist/surface/mcp/server-instructions.js +14 -4
- package/dist/surface/mcp/shared-params.js +2 -2
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
- package/dist/surface/tools/act/settle-in-flight.js +25 -3
- package/dist/surface/tools/act-sequence-tool.js +2 -2
- package/dist/surface/tools/act-tools.js +25 -9
- package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +17 -2
- package/dist/surface/tools/browser-tools.js +8 -1
- package/dist/surface/tools/chat-drives.d.ts +26 -0
- package/dist/surface/tools/chat-drives.js +165 -0
- package/dist/surface/tools/dynamic-tools.js +29 -2
- package/dist/surface/tools/empty-session-list.d.ts +41 -0
- package/dist/surface/tools/empty-session-list.js +80 -0
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +38 -6
- package/dist/surface/tools/explore-tools.d.ts +10 -1
- package/dist/surface/tools/explore-tools.js +82 -13
- package/dist/surface/tools/feedback-tools.js +14 -0
- package/dist/surface/tools/harness-explore.d.ts +96 -14
- package/dist/surface/tools/harness-explore.js +227 -202
- package/dist/surface/tools/harness-plan.d.ts +12 -0
- package/dist/surface/tools/harness-plan.js +31 -0
- package/dist/surface/tools/harness-script.d.ts +8 -0
- package/dist/surface/tools/harness-script.js +342 -0
- package/dist/surface/tools/harness-toolset.d.ts +14 -1
- package/dist/surface/tools/harness-toolset.js +77 -6
- package/dist/surface/tools/invoke-tool.js +64 -9
- package/dist/surface/tools/lease-seed.d.ts +29 -0
- package/dist/surface/tools/lease-seed.js +123 -0
- package/dist/surface/tools/lease-tools.d.ts +3 -23
- package/dist/surface/tools/lease-tools.js +30 -131
- package/dist/surface/tools/live-call-text.js +2 -2
- package/dist/surface/tools/next-step.d.ts +19 -0
- package/dist/surface/tools/next-step.js +159 -0
- package/dist/surface/tools/observe-tools.js +2 -4
- package/dist/surface/tools/platform-moment.d.ts +12 -0
- package/dist/surface/tools/platform-moment.js +168 -0
- package/dist/surface/tools/query-hint-schema.d.ts +7 -0
- package/dist/surface/tools/query-hint-schema.js +10 -0
- package/dist/surface/tools/read-tools.js +8 -2
- package/dist/surface/tools/real-input-attempt.js +96 -13
- package/dist/surface/tools/tool-kit.d.ts +25 -1
- package/dist/surface/tools/tool-kit.js +4 -6
- package/dist/surface/tools/tools.js +20 -48
- package/dist/telemetry/argument-shape.js +1 -1
- package/dist/telemetry/hud-metrics.d.ts +2 -0
- package/dist/telemetry/hud-metrics.js +17 -0
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- package/dist/telemetry/telemetry.d.ts +12 -0
- package/dist/telemetry/telemetry.js +43 -14
- package/dist/telemetry/verification-of.js +1 -0
- package/dist/wire-journal.js +4 -1
- package/docs/README.md +1 -1
- package/docs/actions.mdx +1 -2
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/autodrive.md +29 -25
- package/docs/cli/cloud.mdx +6 -4
- package/docs/cli/doctor.mdx +6 -0
- package/docs/cli/drive.mdx +8 -2
- package/docs/cli/init.mdx +11 -13
- package/docs/cli/status.mdx +5 -1
- package/docs/cli/version.mdx +3 -3
- package/docs/cli.mdx +14 -8
- package/docs/desktop.mdx +5 -1
- package/docs/environment.mdx +3 -7
- package/docs/getting-started.md +14 -2
- package/docs/human-control.md +1 -2
- package/docs/index.mdx +4 -3
- package/docs/install-agentic.mdx +1 -1
- package/docs/install-manual.mdx +2 -2
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/tauri.mdx +2 -2
- package/docs/packages/vite-plugin.mdx +24 -1
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +23 -11
- package/docs/telemetry.md +1 -1
- package/docs/tools/act.mdx +1 -1
- package/docs/tools/session-and-feedback.mdx +9 -9
- package/docs/usage.md +5 -3
- package/docs/what-is-recorded.md +13 -3
- package/docs/what-is-reticle.mdx +3 -1
- package/package.json +8 -12
- package/dist/features/harness/driver.d.ts +0 -50
- package/dist/features/harness/driver.js +0 -236
- package/dist/features/harness/jev-driver.d.ts +0 -188
- package/dist/features/harness/jev-driver.js +0 -678
- package/dist/features/harness/openai-driver.d.ts +0 -46
- package/dist/features/harness/openai-driver.js +0 -194
- package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
- package/dist/memory/project/dir/fill-value-store.js +0 -88
- package/docs/local-registry.md +0 -143
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeded storage for a lease, and the errors a lease's navigation throws: what a caller may see of
|
|
3
|
+
* them without seeing the secrets it seeded. Split from lease-tools.ts, which crossed the line cap.
|
|
4
|
+
*/
|
|
5
|
+
import { REDACTED_VALUE, scrubKnownSecrets } from '@reticlehq/core';
|
|
6
|
+
function redactExactValue(text, value) {
|
|
7
|
+
if (0 === value.length)
|
|
8
|
+
return text;
|
|
9
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
10
|
+
return text.replace(new RegExp(`(^|[^a-zA-Z0-9])${escaped}(?=$|[^a-zA-Z0-9])`, 'g'), `$1${REDACTED_VALUE}`);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Scrub known secret shapes and raw seedStorage values (cookies, localStorage, sessionStorage)
|
|
14
|
+
* from an error string before it reaches daemon logs, metrics, or MCP error responses.
|
|
15
|
+
*/
|
|
16
|
+
export function scrubSeedFromError(text, seed) {
|
|
17
|
+
let out = scrubKnownSecrets(text);
|
|
18
|
+
if (undefined !== seed && null !== seed && 'object' === typeof seed) {
|
|
19
|
+
const s = seed;
|
|
20
|
+
const cookies = s['cookies'];
|
|
21
|
+
if (undefined !== cookies && null !== cookies) {
|
|
22
|
+
if (Array.isArray(cookies)) {
|
|
23
|
+
for (const c of cookies) {
|
|
24
|
+
if (undefined !== c && null !== c && 'object' === typeof c) {
|
|
25
|
+
const val = c['value'];
|
|
26
|
+
if (undefined !== val && 'string' === typeof val) {
|
|
27
|
+
out = redactExactValue(out, val);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if ('object' === typeof cookies) {
|
|
33
|
+
for (const v of Object.values(cookies)) {
|
|
34
|
+
if ('string' === typeof v) {
|
|
35
|
+
out = redactExactValue(out, v);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const local = s['local'];
|
|
41
|
+
if (undefined !== local && null !== local && 'object' === typeof local) {
|
|
42
|
+
for (const v of Object.values(local)) {
|
|
43
|
+
if ('string' === typeof v) {
|
|
44
|
+
out = redactExactValue(out, v);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const session = s['session'];
|
|
49
|
+
if (undefined !== session && null !== session && 'object' === typeof session) {
|
|
50
|
+
for (const v of Object.values(session)) {
|
|
51
|
+
if ('string' === typeof v) {
|
|
52
|
+
out = redactExactValue(out, v);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** True for a Playwright `storageState()` export (`{ origins: [...] }`), not our seed shape. */
|
|
60
|
+
export function looksLikeStorageStateExport(seed) {
|
|
61
|
+
return Array.isArray(seed?.origins);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Turn a raw navigation failure (Playwright's `page.goto: net::ERR_… at <url>\nCall log:…`, often
|
|
65
|
+
* with ANSI codes) into a short, clean reason — so the agent/user sees "is the app running?" instead
|
|
66
|
+
* of an internals-leaking wall of text.
|
|
67
|
+
*/
|
|
68
|
+
export function cleanNavError(err, seed) {
|
|
69
|
+
const rawMsg = err instanceof Error ? err.message : String(err);
|
|
70
|
+
const msg = scrubSeedFromError(rawMsg, seed);
|
|
71
|
+
// Strip ANSI color codes (ESC[…m) Playwright emits — built via fromCharCode to keep the
|
|
72
|
+
// control character out of a regex literal (no-control-regex).
|
|
73
|
+
const ansi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
|
|
74
|
+
const firstLine = (msg.split('\n')[0] ?? msg).replace(ansi, '');
|
|
75
|
+
const netCode = /net::[A-Z_]+/.exec(firstLine);
|
|
76
|
+
if (netCode !== null)
|
|
77
|
+
return netCode[0];
|
|
78
|
+
if (/timeout/i.test(firstLine))
|
|
79
|
+
return 'navigation timed out';
|
|
80
|
+
return firstLine
|
|
81
|
+
.replace(/^page\.goto:\s*/, '')
|
|
82
|
+
.replace(/\s+at\s+https?:\/\/\S+.*$/, '')
|
|
83
|
+
.trim()
|
|
84
|
+
.slice(0, 100);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Determine if a required seedStorage application precondition failed.
|
|
88
|
+
*
|
|
89
|
+
* Checks strong signals:
|
|
90
|
+
* 1. HTTP 401/403 status on the initial navigation.
|
|
91
|
+
* 2. Cross-origin redirect when localStorage or sessionStorage was requested (skipped by origin-scoping).
|
|
92
|
+
* 3. Clear redirect to an authentication/login endpoint when the requested URL was not a login page.
|
|
93
|
+
*
|
|
94
|
+
* Normal app redirects (e.g. / → /dashboard) are valid and return undefined.
|
|
95
|
+
*/
|
|
96
|
+
export function evaluateSeedPrecondition(requestedUrl, actualUrl, seed, navStatus) {
|
|
97
|
+
if (401 === navStatus || 403 === navStatus) {
|
|
98
|
+
return `HTTP ${navStatus} returned on initial seeded navigation to ${requestedUrl}`;
|
|
99
|
+
}
|
|
100
|
+
if (actualUrl === undefined || '' === actualUrl.trim())
|
|
101
|
+
return undefined;
|
|
102
|
+
let reqParsed;
|
|
103
|
+
let actParsed;
|
|
104
|
+
try {
|
|
105
|
+
reqParsed = new URL(requestedUrl);
|
|
106
|
+
actParsed = new URL(actualUrl);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
// Cross-origin redirect when local or session storage was requested
|
|
112
|
+
const hasStorage = (undefined !== seed.local && 0 < Object.keys(seed.local).length) ||
|
|
113
|
+
(undefined !== seed.session && 0 < Object.keys(seed.session).length);
|
|
114
|
+
if (hasStorage && reqParsed.origin !== actParsed.origin) {
|
|
115
|
+
return `seeded storage precondition not established: cross-origin redirect from ${reqParsed.origin} to ${actParsed.origin} skipped origin-scoped storage injection`;
|
|
116
|
+
}
|
|
117
|
+
// Clear redirect to an authentication/login endpoint when the requested URL was not a login page
|
|
118
|
+
const isAuthPath = (pathname) => /(?:^|\/)(?:login|signin|sign-in|auth\/login|auth\/signin|session\/new)(?:$|\/)/i.test(pathname);
|
|
119
|
+
if (!isAuthPath(reqParsed.pathname) && isAuthPath(actParsed.pathname)) {
|
|
120
|
+
return `seeded authentication precondition not established: redirected from ${reqParsed.pathname} to login page (${actParsed.pathname})`;
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
@@ -14,34 +14,14 @@ import { type StorageFixturePort } from '../../portal/input/storage-fixture.js';
|
|
|
14
14
|
import { type NetworkMutationPort } from '../../portal/input/network-mutation.js';
|
|
15
15
|
import type { Perturbation } from '@reticlehq/core';
|
|
16
16
|
import { type SeedStorage } from '@reticlehq/core';
|
|
17
|
+
import { HudVisibility } from '@reticlehq/core';
|
|
17
18
|
import type { ToolDef } from './tool-kit.js';
|
|
18
19
|
/**
|
|
19
20
|
* Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
|
|
20
21
|
* adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
|
|
21
22
|
*/
|
|
22
|
-
export declare function appendReticleParams(url: string, session: string, projectId?: string): string;
|
|
23
|
-
|
|
24
|
-
* Scrub known secret shapes and raw seedStorage values (cookies, localStorage, sessionStorage)
|
|
25
|
-
* from an error string before it reaches daemon logs, metrics, or MCP error responses.
|
|
26
|
-
*/
|
|
27
|
-
export declare function scrubSeedFromError(text: string, seed?: unknown): string;
|
|
28
|
-
/**
|
|
29
|
-
* Turn a raw navigation failure (Playwright's `page.goto: net::ERR_… at <url>\nCall log:…`, often
|
|
30
|
-
* with ANSI codes) into a short, clean reason — so the agent/user sees "is the app running?" instead
|
|
31
|
-
* of an internals-leaking wall of text.
|
|
32
|
-
*/
|
|
33
|
-
export declare function cleanNavError(err: unknown, seed?: SeedStorage): string;
|
|
34
|
-
/**
|
|
35
|
-
* Determine if a required seedStorage application precondition failed.
|
|
36
|
-
*
|
|
37
|
-
* Checks strong signals:
|
|
38
|
-
* 1. HTTP 401/403 status on the initial navigation.
|
|
39
|
-
* 2. Cross-origin redirect when localStorage or sessionStorage was requested (skipped by origin-scoping).
|
|
40
|
-
* 3. Clear redirect to an authentication/login endpoint when the requested URL was not a login page.
|
|
41
|
-
*
|
|
42
|
-
* Normal app redirects (e.g. / → /dashboard) are valid and return undefined.
|
|
43
|
-
*/
|
|
44
|
-
export declare function evaluateSeedPrecondition(requestedUrl: string, actualUrl: string | undefined, seed: SeedStorage, navStatus?: number): string | undefined;
|
|
23
|
+
export declare function appendReticleParams(url: string, session: string, projectId?: string, hud?: HudVisibility): string;
|
|
24
|
+
export { cleanNavError, evaluateSeedPrecondition, scrubSeedFromError } from './lease-seed.js';
|
|
45
25
|
/**
|
|
46
26
|
* The session a lease's tab actually registered as, or undefined while nothing has.
|
|
47
27
|
*
|
|
@@ -20,10 +20,10 @@ import { readProjectFramework, readProjectId } from '../../command/cli/ports/res
|
|
|
20
20
|
import { hasAnyAppConnectedBefore } from '../../memory/recall/prior/connection-memory.js';
|
|
21
21
|
import { AGENT_DRIVING_ELSEWHERE, AGENT_DRIVING_HERE_AGAIN, watchersToNotify, } from '../../portal/session/lease-visibility.js';
|
|
22
22
|
import { reticleStateHome } from '../../command/daemon/daemon.js';
|
|
23
|
-
import { LeaseNotReadyReason,
|
|
24
|
-
import { ReticleTool, SESSION_HEALTH } from '@reticlehq/core';
|
|
23
|
+
import { LeaseNotReadyReason, RETICLE_URL_PARAM, RETICLE_DEFAULT_PORT, ReticleCommand, SeedStorageSchema, } from '@reticlehq/core';
|
|
24
|
+
import { HudVisibility, ReticleTool, SESSION_HEALTH } from '@reticlehq/core';
|
|
25
25
|
import { asString } from '@reticlehq/core';
|
|
26
|
-
import {
|
|
26
|
+
import { chromiumPreflightRefusal } from '../../command/cli/doctor/browser/chromium-hint.js';
|
|
27
27
|
/**
|
|
28
28
|
* Everything the daemon already knows about why a leased tab might not have dialled in.
|
|
29
29
|
*
|
|
@@ -95,13 +95,15 @@ function alreadyHeldHint(sessionId, origin) {
|
|
|
95
95
|
* Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
|
|
96
96
|
* adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
|
|
97
97
|
*/
|
|
98
|
-
export function appendReticleParams(url, session, projectId) {
|
|
98
|
+
export function appendReticleParams(url, session, projectId, hud) {
|
|
99
99
|
try {
|
|
100
100
|
const u = new URL(url);
|
|
101
101
|
u.searchParams.set(RETICLE_URL_PARAM.SESSION, session);
|
|
102
102
|
if (projectId !== undefined && projectId.length > 0) {
|
|
103
103
|
u.searchParams.set(RETICLE_URL_PARAM.PROJECT, projectId);
|
|
104
104
|
}
|
|
105
|
+
if (hud !== undefined)
|
|
106
|
+
u.searchParams.set(RETICLE_URL_PARAM.HUD, hud);
|
|
105
107
|
return u.toString();
|
|
106
108
|
}
|
|
107
109
|
catch {
|
|
@@ -112,124 +114,8 @@ export function appendReticleParams(url, session, projectId) {
|
|
|
112
114
|
return `${url}${sep}${RETICLE_URL_PARAM.SESSION}=${encodeURIComponent(session)}${proj}`;
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return text;
|
|
118
|
-
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
119
|
-
return text.replace(new RegExp(`(^|[^a-zA-Z0-9])${escaped}(?=$|[^a-zA-Z0-9])`, 'g'), `$1${REDACTED_VALUE}`);
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Scrub known secret shapes and raw seedStorage values (cookies, localStorage, sessionStorage)
|
|
123
|
-
* from an error string before it reaches daemon logs, metrics, or MCP error responses.
|
|
124
|
-
*/
|
|
125
|
-
export function scrubSeedFromError(text, seed) {
|
|
126
|
-
let out = scrubKnownSecrets(text);
|
|
127
|
-
if (undefined !== seed && null !== seed && 'object' === typeof seed) {
|
|
128
|
-
const s = seed;
|
|
129
|
-
const cookies = s['cookies'];
|
|
130
|
-
if (undefined !== cookies && null !== cookies) {
|
|
131
|
-
if (Array.isArray(cookies)) {
|
|
132
|
-
for (const c of cookies) {
|
|
133
|
-
if (undefined !== c && null !== c && 'object' === typeof c) {
|
|
134
|
-
const val = c['value'];
|
|
135
|
-
if (undefined !== val && 'string' === typeof val) {
|
|
136
|
-
out = redactExactValue(out, val);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
else if ('object' === typeof cookies) {
|
|
142
|
-
for (const v of Object.values(cookies)) {
|
|
143
|
-
if ('string' === typeof v) {
|
|
144
|
-
out = redactExactValue(out, v);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
const local = s['local'];
|
|
150
|
-
if (undefined !== local && null !== local && 'object' === typeof local) {
|
|
151
|
-
for (const v of Object.values(local)) {
|
|
152
|
-
if ('string' === typeof v) {
|
|
153
|
-
out = redactExactValue(out, v);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const session = s['session'];
|
|
158
|
-
if (undefined !== session && null !== session && 'object' === typeof session) {
|
|
159
|
-
for (const v of Object.values(session)) {
|
|
160
|
-
if ('string' === typeof v) {
|
|
161
|
-
out = redactExactValue(out, v);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return out;
|
|
167
|
-
}
|
|
168
|
-
/** True for a Playwright `storageState()` export (`{ origins: [...] }`), not our seed shape. */
|
|
169
|
-
function looksLikeStorageStateExport(seed) {
|
|
170
|
-
return Array.isArray(seed?.origins);
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Turn a raw navigation failure (Playwright's `page.goto: net::ERR_… at <url>\nCall log:…`, often
|
|
174
|
-
* with ANSI codes) into a short, clean reason — so the agent/user sees "is the app running?" instead
|
|
175
|
-
* of an internals-leaking wall of text.
|
|
176
|
-
*/
|
|
177
|
-
export function cleanNavError(err, seed) {
|
|
178
|
-
const rawMsg = err instanceof Error ? err.message : String(err);
|
|
179
|
-
const msg = scrubSeedFromError(rawMsg, seed);
|
|
180
|
-
// Strip ANSI color codes (ESC[…m) Playwright emits — built via fromCharCode to keep the
|
|
181
|
-
// control character out of a regex literal (no-control-regex).
|
|
182
|
-
const ansi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
|
|
183
|
-
const firstLine = (msg.split('\n')[0] ?? msg).replace(ansi, '');
|
|
184
|
-
const netCode = /net::[A-Z_]+/.exec(firstLine);
|
|
185
|
-
if (netCode !== null)
|
|
186
|
-
return netCode[0];
|
|
187
|
-
if (/timeout/i.test(firstLine))
|
|
188
|
-
return 'navigation timed out';
|
|
189
|
-
return firstLine
|
|
190
|
-
.replace(/^page\.goto:\s*/, '')
|
|
191
|
-
.replace(/\s+at\s+https?:\/\/\S+.*$/, '')
|
|
192
|
-
.trim()
|
|
193
|
-
.slice(0, 100);
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Determine if a required seedStorage application precondition failed.
|
|
197
|
-
*
|
|
198
|
-
* Checks strong signals:
|
|
199
|
-
* 1. HTTP 401/403 status on the initial navigation.
|
|
200
|
-
* 2. Cross-origin redirect when localStorage or sessionStorage was requested (skipped by origin-scoping).
|
|
201
|
-
* 3. Clear redirect to an authentication/login endpoint when the requested URL was not a login page.
|
|
202
|
-
*
|
|
203
|
-
* Normal app redirects (e.g. / → /dashboard) are valid and return undefined.
|
|
204
|
-
*/
|
|
205
|
-
export function evaluateSeedPrecondition(requestedUrl, actualUrl, seed, navStatus) {
|
|
206
|
-
if (401 === navStatus || 403 === navStatus) {
|
|
207
|
-
return `HTTP ${navStatus} returned on initial seeded navigation to ${requestedUrl}`;
|
|
208
|
-
}
|
|
209
|
-
if (actualUrl === undefined || '' === actualUrl.trim())
|
|
210
|
-
return undefined;
|
|
211
|
-
let reqParsed;
|
|
212
|
-
let actParsed;
|
|
213
|
-
try {
|
|
214
|
-
reqParsed = new URL(requestedUrl);
|
|
215
|
-
actParsed = new URL(actualUrl);
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
// Cross-origin redirect when local or session storage was requested
|
|
221
|
-
const hasStorage = (undefined !== seed.local && 0 < Object.keys(seed.local).length) ||
|
|
222
|
-
(undefined !== seed.session && 0 < Object.keys(seed.session).length);
|
|
223
|
-
if (hasStorage && reqParsed.origin !== actParsed.origin) {
|
|
224
|
-
return `seeded storage precondition not established: cross-origin redirect from ${reqParsed.origin} to ${actParsed.origin} skipped origin-scoped storage injection`;
|
|
225
|
-
}
|
|
226
|
-
// Clear redirect to an authentication/login endpoint when the requested URL was not a login page
|
|
227
|
-
const isAuthPath = (pathname) => /(?:^|\/)(?:login|signin|sign-in|auth\/login|auth\/signin|session\/new)(?:$|\/)/i.test(pathname);
|
|
228
|
-
if (!isAuthPath(reqParsed.pathname) && isAuthPath(actParsed.pathname)) {
|
|
229
|
-
return `seeded authentication precondition not established: redirected from ${reqParsed.pathname} to login page (${actParsed.pathname})`;
|
|
230
|
-
}
|
|
231
|
-
return undefined;
|
|
232
|
-
}
|
|
117
|
+
export { cleanNavError, evaluateSeedPrecondition, scrubSeedFromError } from './lease-seed.js';
|
|
118
|
+
import { cleanNavError, evaluateSeedPrecondition, looksLikeStorageStateExport, scrubSeedFromError, } from './lease-seed.js';
|
|
233
119
|
/** A fresh, collision-resistant lease id. Uses crypto at the I/O boundary (not pure logic). */
|
|
234
120
|
function newLeaseId() {
|
|
235
121
|
const uuid = 'function' === typeof globalThis.crypto?.randomUUID
|
|
@@ -437,7 +323,15 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
437
323
|
.string()
|
|
438
324
|
.optional()
|
|
439
325
|
.describe('Stable project id to stamp on the leased tab so the agent can scope to it.'),
|
|
440
|
-
seedStorage: SeedStorageSchema.optional().describe('Initial storage state (
|
|
326
|
+
seedStorage: SeedStorageSchema.optional().describe('Initial storage state (local, session and cookies) to seed before the first navigation (e.g. `seedStorage: { local: { token: "..." } }` to start already authenticated).'),
|
|
327
|
+
headed: z
|
|
328
|
+
.boolean()
|
|
329
|
+
.optional()
|
|
330
|
+
.describe('Open it in a browser window somebody can watch, instead of headless.'),
|
|
331
|
+
hud: z
|
|
332
|
+
.enum([HudVisibility.SHOWN, HudVisibility.HIDDEN, HudVisibility.REMOVED])
|
|
333
|
+
.optional()
|
|
334
|
+
.describe('How the HUD starts on the page: shown (default), hidden, or removed.'),
|
|
441
335
|
},
|
|
442
336
|
outputSchema: {
|
|
443
337
|
sessionId: z.string(),
|
|
@@ -487,12 +381,13 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
487
381
|
// fine. Say the real thing at the first refusal instead. The phrasing carries "Chromium is not
|
|
488
382
|
// installed" so error-recovery routes it to the NO_POOL fix (install + drive a human tab).
|
|
489
383
|
if (deps.browserProbe !== undefined) {
|
|
490
|
-
const
|
|
491
|
-
if (
|
|
492
|
-
throw new Error(
|
|
493
|
-
}
|
|
384
|
+
const refusal = chromiumPreflightRefusal(await deps.browserProbe());
|
|
385
|
+
if (refusal !== undefined)
|
|
386
|
+
throw new Error(refusal);
|
|
494
387
|
}
|
|
495
388
|
const projectId = asString(args['projectId']);
|
|
389
|
+
const headed = true === args['headed'];
|
|
390
|
+
const hud = Object.values(HudVisibility).find((v) => v === args['hud']);
|
|
496
391
|
const seedStorageArg = args['seedStorage'];
|
|
497
392
|
let validatedSeed;
|
|
498
393
|
if (seedStorageArg !== undefined) {
|
|
@@ -526,10 +421,12 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
526
421
|
await priorLock.catch(() => undefined);
|
|
527
422
|
}
|
|
528
423
|
try {
|
|
529
|
-
|
|
424
|
+
// Only a lease THIS caller took: another agent's tab is never reused or released (#1226).
|
|
425
|
+
// A window asked for is never answered with the headless tab on that origin, nor the reverse.
|
|
426
|
+
const held = origin === undefined ? undefined : pool.leaseIdOnOrigin(origin, deps.attachId);
|
|
427
|
+
const existing = held !== undefined && pool.isHeaded(held) === headed ? held : undefined;
|
|
530
428
|
if (validatedSeed !== undefined && existing !== undefined) {
|
|
531
|
-
//
|
|
532
|
-
// release it so the new lease starts clean with the caller-provided state.
|
|
429
|
+
// Seeding asked for: release its own context so the new lease starts with the given state.
|
|
533
430
|
await pool.release(existing);
|
|
534
431
|
}
|
|
535
432
|
else if (existing !== undefined && origin !== undefined) {
|
|
@@ -574,11 +471,13 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
574
471
|
await pool.release(existing);
|
|
575
472
|
}
|
|
576
473
|
const sessionId = newLeaseId();
|
|
577
|
-
const navUrl = appendReticleParams(url, sessionId, projectId);
|
|
474
|
+
const navUrl = appendReticleParams(url, sessionId, projectId, hud);
|
|
578
475
|
let lease;
|
|
579
476
|
try {
|
|
580
477
|
lease = await pool.acquire(navUrl, {
|
|
581
478
|
sessionId,
|
|
479
|
+
...(headed ? { headed } : {}),
|
|
480
|
+
...(deps.attachId === undefined ? {} : { owner: deps.attachId }),
|
|
582
481
|
...(validatedSeed !== undefined ? { seedStorage: validatedSeed } : {}),
|
|
583
482
|
});
|
|
584
483
|
}
|
|
@@ -80,8 +80,8 @@ const NO_TOOL_EQUIVALENT = [
|
|
|
80
80
|
* every session, and the longer two-route version cost 67 bytes there against a budget measured
|
|
81
81
|
* in whole-session terms — for advice that is read once, on a first run.
|
|
82
82
|
*
|
|
83
|
-
* `explore`
|
|
84
|
-
* the same breath (
|
|
83
|
+
* `explore` runs on the platform and says so itself when it is not linked, naming the fallback in
|
|
84
|
+
* the same breath (drive the journey yourself with the MCP tools). That
|
|
85
85
|
* error is now recognised as ours, so the reader who hits it gets the next step instead of an
|
|
86
86
|
* invitation to file a defect report. One hop, and every hop is honest.
|
|
87
87
|
*/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const NextText: {
|
|
2
|
+
readonly DECLARE: "Record what the user asked, once per task, verbatim: reticle_run { tool: \"reticle_intent\", args: { action: \"declare\", request: \"<their words>\" } }. It travels with every run.";
|
|
3
|
+
readonly SYNC_PROBLEM: (why: string) => string;
|
|
4
|
+
readonly CONNECT: "These runs stay on this machine. To share them on the team dashboard, the user runs once: npx @reticlehq/server connect";
|
|
5
|
+
readonly FINISH_LINKED: "Done driving? reticle_session { action: \"yield\" } hands the tab back; this run syncs to the platform on its own.";
|
|
6
|
+
readonly FINISH: "Done driving? reticle_session { action: \"yield\" } hands the tab back.";
|
|
7
|
+
readonly REPLAY: (flows: number) => string;
|
|
8
|
+
};
|
|
9
|
+
/** Tests only. */
|
|
10
|
+
export declare function resetNextStep(): void;
|
|
11
|
+
export interface NextStepInput {
|
|
12
|
+
tool: string;
|
|
13
|
+
/** Whether this result carries a verdict. */
|
|
14
|
+
verdict: boolean;
|
|
15
|
+
root: string | undefined;
|
|
16
|
+
now: number;
|
|
17
|
+
}
|
|
18
|
+
/** The line, or undefined when there is nothing the agent should be told right now. */
|
|
19
|
+
export declare function nextStep(input: NextStepInput): string | undefined;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ReticleDir, ReticleEnv } from '@reticlehq/core';
|
|
4
|
+
/**
|
|
5
|
+
* The one thing the agent should do next, on the result of the call it just made.
|
|
6
|
+
*
|
|
7
|
+
* Every client reads tool results; few read a briefing twice, and none keep a skill in mind across a
|
|
8
|
+
* long session. So what an agent must not forget (record what the user asked, hand the tab back,
|
|
9
|
+
* notice when its runs are not reaching the platform, tell the user how to connect) is said where
|
|
10
|
+
* the agent is already looking, one short line, on the moment it applies. It is placed first in the
|
|
11
|
+
* result, because a model that truncates a long result keeps its beginning.
|
|
12
|
+
*
|
|
13
|
+
* The daemon syncs runs itself, whichever agent drove them; nothing here asks an agent to push.
|
|
14
|
+
* It only says when the sync needs a person (a refusal, a failure, a project never connected).
|
|
15
|
+
*/
|
|
16
|
+
/** How long a declared request counts as "the current task". Matches the run store's window. */
|
|
17
|
+
const REQUEST_FRESH_MS = 6 * 60 * 60 * 1000;
|
|
18
|
+
/** A reminder that is not about a verdict is repeated at most this often, per project. */
|
|
19
|
+
const QUIET_CALLS = 10;
|
|
20
|
+
/** An unlinked project hears how to connect on every Nth verdict, the first included. */
|
|
21
|
+
const CONNECT_EVERY = 5;
|
|
22
|
+
/**
|
|
23
|
+
* How often the agent is asked to record the request before the other lines get their turn. In
|
|
24
|
+
* every recorded run the agent ignored it, and because it always won, no agent was ever told its
|
|
25
|
+
* runs were not reaching the platform.
|
|
26
|
+
*/
|
|
27
|
+
const DECLARE_ASKS = 2;
|
|
28
|
+
/** A run file this much newer than the last push, and this old, was not sent. */
|
|
29
|
+
const UNSENT_AFTER_MS = 2 * 60 * 1000;
|
|
30
|
+
export const NextText = {
|
|
31
|
+
DECLARE: 'Record what the user asked, once per task, verbatim: reticle_run { tool: "reticle_intent", args: { action: "declare", request: "<their words>" } }. It travels with every run.',
|
|
32
|
+
SYNC_PROBLEM: (why) => `Runs are not reaching the platform (${why}). reticle_run { tool: "reticle_project", args: { push: true } } retries and reports why.`,
|
|
33
|
+
CONNECT: 'These runs stay on this machine. To share them on the team dashboard, the user runs once: npx @reticlehq/server connect',
|
|
34
|
+
FINISH_LINKED: 'Done driving? reticle_session { action: "yield" } hands the tab back; this run syncs to the platform on its own.',
|
|
35
|
+
FINISH: 'Done driving? reticle_session { action: "yield" } hands the tab back.',
|
|
36
|
+
REPLAY: (flows) => `This app has ${String(flows)} saved flow(s). Replay them before driving anything by hand: reticle_verify { action: "flows" } re-verifies them with no model.`,
|
|
37
|
+
};
|
|
38
|
+
const roots = new Map();
|
|
39
|
+
/** Tests only. */
|
|
40
|
+
export function resetNextStep() {
|
|
41
|
+
roots.clear();
|
|
42
|
+
}
|
|
43
|
+
function readJson(path) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
46
|
+
return 'object' === typeof parsed && null !== parsed
|
|
47
|
+
? parsed
|
|
48
|
+
: undefined;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Declared within the window, read from the file's own stamp (or its age when it has none). */
|
|
55
|
+
function requestDeclared(root, now) {
|
|
56
|
+
const path = join(root, ReticleDir.REQUEST_FILE);
|
|
57
|
+
if (!existsSync(path))
|
|
58
|
+
return false;
|
|
59
|
+
const at = readJson(path)?.['at'];
|
|
60
|
+
const when = 'number' === typeof at ? at : statSync(path).mtimeMs;
|
|
61
|
+
return now - when < REQUEST_FRESH_MS;
|
|
62
|
+
}
|
|
63
|
+
/** Run files written after the last push and old enough that a sync cycle should have sent them. */
|
|
64
|
+
function unsentRuns(root, lastPushAt, now) {
|
|
65
|
+
try {
|
|
66
|
+
const dir = join(root, ReticleDir.RUNS_SUBDIR);
|
|
67
|
+
return readdirSync(dir).filter((name) => {
|
|
68
|
+
const at = statSync(join(dir, name)).mtimeMs;
|
|
69
|
+
return at > lastPushAt + UNSENT_AFTER_MS && now - at > UNSENT_AFTER_MS;
|
|
70
|
+
}).length;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Why sync needs a person, from the sync's own bookkeeping; undefined when it is fine.
|
|
78
|
+
*
|
|
79
|
+
* Runs that were written and never sent count too. A project the sync loop never looked at has no
|
|
80
|
+
* error and no refusal, which is how a whole day of runs stayed on one machine with every check
|
|
81
|
+
* reporting fine.
|
|
82
|
+
*/
|
|
83
|
+
function syncProblem(root, now) {
|
|
84
|
+
const state = readJson(join(root, ReticleDir.CLOUD_STATE_FILE));
|
|
85
|
+
const pushed = state?.['lastPushAt'];
|
|
86
|
+
const unsent = unsentRuns(root, 'number' === typeof pushed ? pushed : 0, now);
|
|
87
|
+
if (0 < unsent)
|
|
88
|
+
return `${String(unsent)} run(s) written here were never sent`;
|
|
89
|
+
if (state === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
const error = state['lastError'];
|
|
92
|
+
if ('string' === typeof error && 0 < error.length)
|
|
93
|
+
return error.slice(0, 120);
|
|
94
|
+
const refused = state['refusedRuns'];
|
|
95
|
+
const count = 'object' === typeof refused && null !== refused ? Object.keys(refused).length : 0;
|
|
96
|
+
return 0 < count ? `${String(count)} run(s) refused` : undefined;
|
|
97
|
+
}
|
|
98
|
+
/** Saved flow files under `flows/`, one level of project folders deep. */
|
|
99
|
+
function savedFlows(dir) {
|
|
100
|
+
try {
|
|
101
|
+
return readdirSync(dir, { withFileTypes: true }).reduce((n, entry) => n +
|
|
102
|
+
(entry.isDirectory()
|
|
103
|
+
? readdirSync(join(dir, entry.name)).filter((f) => f.endsWith('.json')).length
|
|
104
|
+
: entry.name.endsWith('.json')
|
|
105
|
+
? 1
|
|
106
|
+
: 0), 0);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** The line, or undefined when there is nothing the agent should be told right now. */
|
|
113
|
+
export function nextStep(input) {
|
|
114
|
+
const { root } = input;
|
|
115
|
+
if (root === undefined)
|
|
116
|
+
return undefined;
|
|
117
|
+
const state = roots.get(root) ?? {
|
|
118
|
+
calls: 0,
|
|
119
|
+
verdicts: 0,
|
|
120
|
+
lastQuietAt: Number.NEGATIVE_INFINITY,
|
|
121
|
+
declareAsks: 0,
|
|
122
|
+
replayTold: false,
|
|
123
|
+
};
|
|
124
|
+
roots.set(root, state);
|
|
125
|
+
state.calls += 1;
|
|
126
|
+
if (input.verdict)
|
|
127
|
+
state.verdicts += 1;
|
|
128
|
+
const linked = existsSync(join(root, ReticleDir.CLOUD_LINK_FILE));
|
|
129
|
+
// A key in the environment syncs too, so its runs not arriving is just as much a problem.
|
|
130
|
+
const sends = linked || 0 < (process.env[ReticleEnv.API_KEY] ?? '').length;
|
|
131
|
+
const quietDue = state.calls - state.lastQuietAt >= QUIET_CALLS;
|
|
132
|
+
// Measured on a whole-app drive: an agent re-drove everything by hand while saved flows sat
|
|
133
|
+
// unreplayed, though the skill says to replay first. Said on the first call, where it is in time.
|
|
134
|
+
if (!state.replayTold && !input.tool.endsWith('verify')) {
|
|
135
|
+
state.replayTold = true;
|
|
136
|
+
const saved = savedFlows(join(root, ReticleDir.FLOWS_SUBDIR));
|
|
137
|
+
if (0 < saved)
|
|
138
|
+
return NextText.REPLAY(saved);
|
|
139
|
+
}
|
|
140
|
+
const problem = sends ? syncProblem(root, input.now) : undefined;
|
|
141
|
+
if (problem !== undefined && (input.verdict || quietDue)) {
|
|
142
|
+
state.lastQuietAt = state.calls;
|
|
143
|
+
return NextText.SYNC_PROBLEM(problem);
|
|
144
|
+
}
|
|
145
|
+
// Not on the declare call itself, which is the answer to this line.
|
|
146
|
+
if (!input.tool.endsWith('intent') &&
|
|
147
|
+
state.declareAsks < DECLARE_ASKS &&
|
|
148
|
+
!requestDeclared(root, input.now) &&
|
|
149
|
+
(input.verdict || quietDue)) {
|
|
150
|
+
state.lastQuietAt = state.calls;
|
|
151
|
+
state.declareAsks += 1;
|
|
152
|
+
return NextText.DECLARE;
|
|
153
|
+
}
|
|
154
|
+
if (!input.verdict)
|
|
155
|
+
return undefined;
|
|
156
|
+
if (!sends && 1 === state.verdicts % CONNECT_EVERY)
|
|
157
|
+
return NextText.CONNECT;
|
|
158
|
+
return sends ? NextText.FINISH_LINKED : NextText.FINISH;
|
|
159
|
+
}
|
|
@@ -258,7 +258,7 @@ export const OBSERVE_TOOLS = [
|
|
|
258
258
|
if (bodyRefusal !== undefined)
|
|
259
259
|
throw new Error(bodyRefusal);
|
|
260
260
|
// Honesty: explicit since wins; else default to the last act's cursor; else the whole buffer.
|
|
261
|
-
|
|
261
|
+
const since = asNumber(args['since']) ?? session.lastAct.cursor() ?? 0;
|
|
262
262
|
// See the note on the assert handler below: a wait cut off by a full-document navigation is
|
|
263
263
|
// followed to the document that took over, rather than graded as a lost observation there.
|
|
264
264
|
const predicateStarted = session.elapsed();
|
|
@@ -270,8 +270,6 @@ export const OBSERVE_TOOLS = [
|
|
|
270
270
|
predicateStarted,
|
|
271
271
|
reevaluate: (next, budget) => waitForPredicate(next, predicate, budget, 0),
|
|
272
272
|
});
|
|
273
|
-
if (followed.followed)
|
|
274
|
-
since = 0;
|
|
275
273
|
session = followed.session;
|
|
276
274
|
const verdict = followed.verdict;
|
|
277
275
|
// match reticle_assert — wrap with control + session health (throttle matters most while blocking)
|
|
@@ -452,7 +450,7 @@ export const OBSERVE_TOOLS = [
|
|
|
452
450
|
// explicit `since` may reach back past the last step, where replay would not find it. The
|
|
453
451
|
// store itself refuses when a navigation came after that step: see RecordingStore.markNavigated.
|
|
454
452
|
if (Verified.YES === decision['verified'] && args['since'] === undefined) {
|
|
455
|
-
captureAssertion(deps.recordings, predicate);
|
|
453
|
+
captureAssertion(deps.recordings, predicate, session.id);
|
|
456
454
|
}
|
|
457
455
|
return withControl(session, {
|
|
458
456
|
...decision,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ToolDeps } from './tool-kit.js';
|
|
2
|
+
/** Verified runs kept only on this machine before the platform is worth a sentence. */
|
|
3
|
+
export declare const LOCAL_RUNS_MOMENT = 5;
|
|
4
|
+
/** A `.reticle` this large is history worth keeping somewhere other than one laptop. */
|
|
5
|
+
export declare const LARGE_RETICLE_BYTES: number;
|
|
6
|
+
export declare function takePlatformMoment(deps: Pick<ToolDeps, 'fs' | 'linkedCloud'> & {
|
|
7
|
+
now?: () => number;
|
|
8
|
+
}, raw: Record<string, unknown>,
|
|
9
|
+
/** The `.reticle` of the project the verdict was about — the session's, not the daemon's. */
|
|
10
|
+
rootOf: () => string): Promise<string | undefined>;
|
|
11
|
+
/** Tests only. */
|
|
12
|
+
export declare function resetPlatformMoments(): void;
|