@reticlehq/server 3.5.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/cli-flow-commands.js +4 -8
- package/dist/command/cli/cli-parse.d.ts +1 -1
- package/dist/command/cli/cli-parse.js +10 -10
- 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 +51 -12
- package/dist/command/cli/cloud-login.d.ts +8 -0
- package/dist/command/cli/cloud-login.js +36 -2
- 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 +6 -4
- 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 +28 -22
- 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 +8 -0
- package/dist/command/setup/bringup/dev-server-wait.js +19 -0
- 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 +18 -1
- package/dist/command/setup/node-effects.js +2 -1
- package/dist/command/setup/remaining-steps.js +2 -1
- package/dist/command/setup/run-setup.d.ts +5 -0
- package/dist/command/setup/run-setup.js +35 -27
- package/dist/command/setup/setup-command.d.ts +2 -1
- package/dist/command/setup/setup-command.js +2 -0
- 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 -73
- package/dist/features/harness/fill-values.js +3 -169
- 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 +76 -82
- 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 +12 -3
- package/dist/memory/cloud/sync-cycle.js +16 -3
- 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 +80 -2
- package/dist/memory/journal/envelope.d.ts +8 -8
- 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 +26 -1
- package/dist/memory/project/artifact-root-resolver.js +94 -13
- 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 +3 -1
- package/dist/portal/bridge/bridge.js +19 -2
- 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 +3 -3
- 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 +7 -2
- package/dist/portal/session/session.js +21 -18
- 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.js +11 -1
- 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 +27 -13
- 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 +23 -7
- 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/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 +37 -4
- 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 -206
- 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 +52 -1
- 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 +15 -1
- package/dist/surface/tools/tool-kit.js +4 -6
- package/dist/surface/tools/tools.js +20 -48
- package/dist/telemetry/hud-metrics.d.ts +2 -0
- package/dist/telemetry/hud-metrics.js +17 -0
- 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/agent-cheatsheet.md +1 -1
- package/docs/autodrive.md +29 -25
- 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 +1 -1
- package/docs/desktop.mdx +5 -1
- package/docs/environment.mdx +3 -7
- package/docs/getting-started.md +13 -11
- package/docs/human-control.md +1 -2
- package/docs/index.mdx +4 -3
- package/docs/install-manual.mdx +2 -2
- package/docs/packages/tauri.mdx +2 -2
- package/docs/packages/vite-plugin.mdx +22 -1
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +17 -7
- package/docs/telemetry.md +1 -1
- package/docs/tools/session-and-feedback.mdx +9 -9
- package/docs/usage.md +5 -2
- 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 -58
- package/dist/features/harness/openai-driver.js +0 -206
- 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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* the thin Playwright adapter that satisfies these interfaces lives separately.
|
|
11
11
|
*/
|
|
12
12
|
import { type SeedStorage } from '@reticlehq/core';
|
|
13
|
-
import type { Launcher, PooledMockRule } from './pool-contract.js';
|
|
13
|
+
import type { Launcher, PooledMockRule, ScreenshotOptions } from './pool-contract.js';
|
|
14
14
|
export type { InitScriptHandle, Launcher, PooledBrowser, PooledContext, PooledCookie, PooledDialog, PooledMockRule, PooledPage, } from './pool-contract.js';
|
|
15
15
|
/** A leased context+page for one flow. `release` frees the slot and closes the context. */
|
|
16
16
|
export interface Lease {
|
|
@@ -42,9 +42,17 @@ interface BrowserPoolOptions {
|
|
|
42
42
|
* the ~450KB build is only read if some page ever needs it; undefined when there is none to give.
|
|
43
43
|
*/
|
|
44
44
|
zeroInstallScript?: () => string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Launches a browser with a window, for a lease somebody wants to watch. Launched only when the
|
|
47
|
+
* first headed lease is asked for. Absent: a headed lease is refused.
|
|
48
|
+
*/
|
|
49
|
+
launchHeaded?: Launcher;
|
|
45
50
|
}
|
|
51
|
+
/** What a headed lease is refused with where no window can be opened. */
|
|
52
|
+
export declare const NO_HEADED_BROWSER = "no headed browser can be opened here";
|
|
46
53
|
/**
|
|
47
|
-
* Owns a
|
|
54
|
+
* Owns a headless browser (and, when asked, a headed one) and leases isolated contexts out of them,
|
|
55
|
+
* capped at `maxContexts` across both.
|
|
48
56
|
* Not exported as a singleton — the daemon owns one instance.
|
|
49
57
|
*/
|
|
50
58
|
export declare class BrowserPool {
|
|
@@ -60,15 +68,17 @@ export declare class BrowserPool {
|
|
|
60
68
|
/** The sessionIds of every currently leased context — used to report/group leased sessions. */
|
|
61
69
|
leasedSessionIds(): string[];
|
|
62
70
|
/**
|
|
63
|
-
* The public session id of an active lease on this origin, if
|
|
71
|
+
* The public session id of an active lease on this origin that `owner` took, if there is one.
|
|
64
72
|
*
|
|
65
73
|
* The lease TOOL reuses that id instead of minting a second context: a second acquire on the same
|
|
66
|
-
* origin used to leave both tabs connected and poison default session resolution (#600).
|
|
67
|
-
*
|
|
74
|
+
* origin used to leave both tabs connected and poison default session resolution (#600). Only the
|
|
75
|
+
* caller that took the lease gets it back: handing it to whoever asked put two agents in one tab,
|
|
76
|
+
* each one's verdicts resting on the other's actions (#1226). A caller with no owner cannot be told
|
|
77
|
+
* apart from another, so it always gets its own. The pool's own `acquire` still mints.
|
|
68
78
|
*
|
|
69
79
|
* Prefers the alias the agent was given, when the app registered under its own name.
|
|
70
80
|
*/
|
|
71
|
-
leaseIdOnOrigin(origin: string): string | undefined;
|
|
81
|
+
leaseIdOnOrigin(origin: string, owner: string | undefined): string | undefined;
|
|
72
82
|
/**
|
|
73
83
|
* Release a lease by its sessionId (closes the context, frees the slot). No-op if unknown.
|
|
74
84
|
*
|
|
@@ -106,9 +116,7 @@ export declare class BrowserPool {
|
|
|
106
116
|
* Touches the lease like any other tool call, so capturing keeps it alive rather than letting the
|
|
107
117
|
* reaper take it out from under a diff.
|
|
108
118
|
*/
|
|
109
|
-
screenshotLease(sessionId: string, opts?:
|
|
110
|
-
fullPage?: boolean;
|
|
111
|
-
}): Promise<Uint8Array | undefined>;
|
|
119
|
+
screenshotLease(sessionId: string, opts?: ScreenshotOptions): Promise<Uint8Array | undefined>;
|
|
112
120
|
/**
|
|
113
121
|
* Move the real pointer on a leased page, or false when this session is not a lease or cannot
|
|
114
122
|
* hover. Same optional-capability contract as screenshotLease: absence must read as "could not
|
|
@@ -167,6 +175,9 @@ export declare class BrowserPool {
|
|
|
167
175
|
signal?: AbortSignal;
|
|
168
176
|
sessionId?: string;
|
|
169
177
|
seedStorage?: SeedStorage;
|
|
178
|
+
owner?: string;
|
|
179
|
+
/** In a browser with a window the person can watch. */
|
|
180
|
+
headed?: boolean;
|
|
170
181
|
}): Promise<Lease>;
|
|
171
182
|
/**
|
|
172
183
|
* The websocket address this lease's page reported it could not reach, if it reported one.
|
|
@@ -181,4 +192,6 @@ export declare class BrowserPool {
|
|
|
181
192
|
lastDialogMessage(sessionId: string): string | undefined;
|
|
182
193
|
/** Close every context and the browser. Pending waiters are rejected (the pool is terminal now). */
|
|
183
194
|
shutdown(): Promise<void>;
|
|
195
|
+
/** Whether this lease lives in the headed browser. */
|
|
196
|
+
isHeaded(sessionId: string): boolean;
|
|
184
197
|
}
|
|
@@ -20,20 +20,22 @@ const DEFAULT_NAV_TIMEOUT_MS = 30_000;
|
|
|
20
20
|
* this is slack for a burst sweep, not a history.
|
|
21
21
|
*/
|
|
22
22
|
const MAX_REMEMBERED_REAPED_LEASES = 64;
|
|
23
|
+
/** What a headed lease is refused with where no window can be opened. */
|
|
24
|
+
export const NO_HEADED_BROWSER = 'no headed browser can be opened here';
|
|
23
25
|
/**
|
|
24
|
-
* Owns a
|
|
26
|
+
* Owns a headless browser (and, when asked, a headed one) and leases isolated contexts out of them,
|
|
27
|
+
* capped at `maxContexts` across both.
|
|
25
28
|
* Not exported as a singleton — the daemon owns one instance.
|
|
26
29
|
*/
|
|
27
30
|
export class BrowserPool {
|
|
28
|
-
#launch;
|
|
29
31
|
#max;
|
|
30
32
|
#genId;
|
|
31
33
|
#now;
|
|
32
34
|
#ttl;
|
|
33
35
|
#zeroInstall;
|
|
34
36
|
#navTimeout;
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
+
#headless;
|
|
38
|
+
#headed;
|
|
37
39
|
#closed = false;
|
|
38
40
|
/** Leases reclaimed for going stale — see reapedLeaseCount(). */
|
|
39
41
|
#reapedLeases = 0;
|
|
@@ -65,7 +67,8 @@ export class BrowserPool {
|
|
|
65
67
|
constructor(launch, opts) {
|
|
66
68
|
if (opts.maxContexts < 1)
|
|
67
69
|
throw new Error('maxContexts must be >= 1');
|
|
68
|
-
this.#
|
|
70
|
+
this.#headless = { launch, headed: false };
|
|
71
|
+
this.#headed = { launch: opts.launchHeaded, headed: true };
|
|
69
72
|
this.#max = opts.maxContexts;
|
|
70
73
|
this.#genId = opts.genSessionId;
|
|
71
74
|
this.#now = opts.now ?? (() => Date.now());
|
|
@@ -93,16 +96,22 @@ export class BrowserPool {
|
|
|
93
96
|
return [...this.#active.keys()];
|
|
94
97
|
}
|
|
95
98
|
/**
|
|
96
|
-
* The public session id of an active lease on this origin, if
|
|
99
|
+
* The public session id of an active lease on this origin that `owner` took, if there is one.
|
|
97
100
|
*
|
|
98
101
|
* The lease TOOL reuses that id instead of minting a second context: a second acquire on the same
|
|
99
|
-
* origin used to leave both tabs connected and poison default session resolution (#600).
|
|
100
|
-
*
|
|
102
|
+
* origin used to leave both tabs connected and poison default session resolution (#600). Only the
|
|
103
|
+
* caller that took the lease gets it back: handing it to whoever asked put two agents in one tab,
|
|
104
|
+
* each one's verdicts resting on the other's actions (#1226). A caller with no owner cannot be told
|
|
105
|
+
* apart from another, so it always gets its own. The pool's own `acquire` still mints.
|
|
101
106
|
*
|
|
102
107
|
* Prefers the alias the agent was given, when the app registered under its own name.
|
|
103
108
|
*/
|
|
104
|
-
leaseIdOnOrigin(origin) {
|
|
109
|
+
leaseIdOnOrigin(origin, owner) {
|
|
110
|
+
if (owner === undefined)
|
|
111
|
+
return undefined;
|
|
105
112
|
for (const [leaseId, lease] of this.#active) {
|
|
113
|
+
if (lease.owner !== owner)
|
|
114
|
+
continue;
|
|
106
115
|
let leaseOrigin;
|
|
107
116
|
try {
|
|
108
117
|
leaseOrigin = new URL(lease.url).origin;
|
|
@@ -314,6 +323,10 @@ export class BrowserPool {
|
|
|
314
323
|
async acquire(url, opts = {}) {
|
|
315
324
|
if (this.#closed)
|
|
316
325
|
throw new Error('browser pool is shut down');
|
|
326
|
+
const slot = true === opts.headed ? this.#headed : this.#headless;
|
|
327
|
+
// Refused before a slot is claimed, so a refusal never holds one.
|
|
328
|
+
if (slot.launch === undefined)
|
|
329
|
+
throw new Error(NO_HEADED_BROWSER);
|
|
317
330
|
// #waitForSlot claims the slot synchronously (bumps #occupied) before returning, so the cap holds
|
|
318
331
|
// even when many acquires race through the gate in the same tick.
|
|
319
332
|
await this.#waitForSlot(opts.signal);
|
|
@@ -322,7 +335,7 @@ export class BrowserPool {
|
|
|
322
335
|
let pending;
|
|
323
336
|
let pendingDialogMessage;
|
|
324
337
|
try {
|
|
325
|
-
const browser = await this.#ensureBrowser();
|
|
338
|
+
const browser = await this.#ensureBrowser(slot);
|
|
326
339
|
context = await browser.newContext();
|
|
327
340
|
const page = await context.newPage();
|
|
328
341
|
// Per-page crash isolation: if THIS renderer dies, reclaim only this lease — the shared browser
|
|
@@ -375,7 +388,7 @@ export class BrowserPool {
|
|
|
375
388
|
// #occupied. Registering the lease now would resurrect a dead entry against a crashed browser with
|
|
376
389
|
// the slot count out of sync (drifting below #active.size, eventually exceeding the cap). If we're
|
|
377
390
|
// no longer the live browser, bail — the catch below closes the context and returns the slot.
|
|
378
|
-
if (
|
|
391
|
+
if (slot.browser !== browser)
|
|
379
392
|
throw new Error('browser crashed during navigation');
|
|
380
393
|
let navStatus;
|
|
381
394
|
if (null !== navRes && 'object' === typeof navRes && 'status' in navRes) {
|
|
@@ -395,9 +408,11 @@ export class BrowserPool {
|
|
|
395
408
|
}
|
|
396
409
|
}
|
|
397
410
|
this.#active.set(sessionId, {
|
|
411
|
+
headed: slot.headed,
|
|
398
412
|
context,
|
|
399
413
|
page,
|
|
400
414
|
url,
|
|
415
|
+
...(opts.owner === undefined ? {} : { owner: opts.owner }),
|
|
401
416
|
touchedAt: this.#now(),
|
|
402
417
|
...(pending === undefined ? {} : { dialFailureUrl: pending }),
|
|
403
418
|
...(pendingDialogMessage === undefined ? {} : { lastDialogMessage: pendingDialogMessage }),
|
|
@@ -450,17 +465,19 @@ export class BrowserPool {
|
|
|
450
465
|
await Promise.all(contexts.map((c) => c.close().catch(() => undefined)));
|
|
451
466
|
// If a launch is in flight, await it so we can close the resulting browser — otherwise it
|
|
452
467
|
// resolves after shutdown returns and the Chromium process is orphaned (hundreds of MB leaked).
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
468
|
+
for (const slot of [this.#headless, this.#headed]) {
|
|
469
|
+
const inflight = slot.launching;
|
|
470
|
+
if (inflight !== undefined) {
|
|
471
|
+
const launched = await inflight.catch(() => undefined);
|
|
472
|
+
if (launched !== undefined && launched !== slot.browser) {
|
|
473
|
+
await launched.close().catch(() => undefined);
|
|
474
|
+
}
|
|
458
475
|
}
|
|
476
|
+
const browser = slot.browser;
|
|
477
|
+
slot.browser = undefined;
|
|
478
|
+
if (browser !== undefined)
|
|
479
|
+
await browser.close().catch(() => undefined);
|
|
459
480
|
}
|
|
460
|
-
const browser = this.#browser;
|
|
461
|
-
this.#browser = undefined;
|
|
462
|
-
if (browser !== undefined)
|
|
463
|
-
await browser.close().catch(() => undefined);
|
|
464
481
|
for (const waiter of this.#waiters.splice(0))
|
|
465
482
|
waiter();
|
|
466
483
|
}
|
|
@@ -532,38 +549,56 @@ export class BrowserPool {
|
|
|
532
549
|
if (next !== undefined)
|
|
533
550
|
next();
|
|
534
551
|
}
|
|
535
|
-
async #ensureBrowser() {
|
|
552
|
+
async #ensureBrowser(slot) {
|
|
536
553
|
if (this.#closed)
|
|
537
554
|
throw new Error('browser pool is shut down');
|
|
538
|
-
if (
|
|
539
|
-
return
|
|
555
|
+
if (slot.browser !== undefined && slot.browser.isConnected())
|
|
556
|
+
return slot.browser;
|
|
557
|
+
const launch = slot.launch;
|
|
558
|
+
if (launch === undefined)
|
|
559
|
+
throw new Error(NO_HEADED_BROWSER);
|
|
540
560
|
// De-dupe concurrent launches: the first acquire to find no browser starts one; the rest await it.
|
|
541
|
-
if (
|
|
561
|
+
if (slot.launching === undefined) {
|
|
542
562
|
// The in-flight promise MUST be cleared on failure as well as success. It used to be cleared only
|
|
543
563
|
// in the success callback, so the first failed launch — Chromium not downloaded, a transient
|
|
544
564
|
// EAGAIN — left a permanently rejected promise here, and every subsequent acquire for the daemon's
|
|
545
565
|
// lifetime re-returned that same stale rejection. Running `npx playwright install` did not help;
|
|
546
566
|
// only restarting the daemon did. A retry must be allowed to actually retry.
|
|
547
|
-
|
|
567
|
+
slot.launching = launch()
|
|
548
568
|
.then((b) => {
|
|
549
|
-
b.onDisconnected(() => this.#onCrash(b));
|
|
550
|
-
|
|
569
|
+
b.onDisconnected(() => this.#onCrash(slot, b));
|
|
570
|
+
slot.browser = b;
|
|
551
571
|
return b;
|
|
552
572
|
})
|
|
553
573
|
.finally(() => {
|
|
554
|
-
|
|
574
|
+
slot.launching = undefined;
|
|
555
575
|
});
|
|
556
576
|
}
|
|
557
|
-
return
|
|
577
|
+
return slot.launching;
|
|
558
578
|
}
|
|
559
|
-
/**
|
|
560
|
-
|
|
561
|
-
|
|
579
|
+
/**
|
|
580
|
+
* A browser process died: drop the leases it held (they're invalid) and clear its slot so the next
|
|
581
|
+
* acquire of that kind relaunches it. The other browser's leases live on.
|
|
582
|
+
*/
|
|
583
|
+
#onCrash(slot, crashed) {
|
|
584
|
+
if (slot.browser !== crashed)
|
|
562
585
|
return; // a stale handler from a prior browser
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
586
|
+
slot.browser = undefined;
|
|
587
|
+
let freed = 0;
|
|
588
|
+
for (const [id, lease] of this.#active) {
|
|
589
|
+
if (lease.headed !== slot.headed)
|
|
590
|
+
continue;
|
|
591
|
+
this.#active.delete(id);
|
|
592
|
+
freed += 1;
|
|
593
|
+
}
|
|
594
|
+
// A lease mid-acquire on the dead browser holds a slot too; it frees it in its own catch. When
|
|
595
|
+
// every lease belonged to this browser, nothing else can be holding one.
|
|
596
|
+
this.#occupied = 0 === this.#active.size ? 0 : Math.max(0, this.#occupied - freed);
|
|
566
597
|
for (const waiter of this.#waiters.splice(0))
|
|
567
598
|
waiter(); // let them re-claim + relaunch
|
|
568
599
|
}
|
|
600
|
+
/** Whether this lease lives in the headed browser. */
|
|
601
|
+
isHeaded(sessionId) {
|
|
602
|
+
return true === this.#active.get(this.#leaseIdOf(sessionId))?.headed;
|
|
603
|
+
}
|
|
569
604
|
}
|
|
@@ -6,6 +6,19 @@
|
|
|
6
6
|
* Headless by default — the pool exists for fault-tolerant headless multi-agent testing.
|
|
7
7
|
*/
|
|
8
8
|
import type { Launcher } from './browser-pool.js';
|
|
9
|
+
import type { ScreenshotOptions } from './pool-contract.js';
|
|
10
|
+
/** Reticle's own panel, kept out of a picture of the app: it is ours, not what the app shows. */
|
|
11
|
+
export declare const HIDE_RETICLE_CHROME_CSS = "[data-reticle-overlay]{display:none !important}";
|
|
12
|
+
/**
|
|
13
|
+
* How Playwright captures a leased tab. A live picture (JPEG) leaves Reticle's panel out, as the
|
|
14
|
+
* visual captures do; a plain PNG capture is unchanged, so no existing baseline moves.
|
|
15
|
+
*/
|
|
16
|
+
export declare function screenshotOptions(opts: ScreenshotOptions): {
|
|
17
|
+
fullPage: boolean;
|
|
18
|
+
type?: 'jpeg';
|
|
19
|
+
quality?: number;
|
|
20
|
+
style?: string;
|
|
21
|
+
};
|
|
9
22
|
/**
|
|
10
23
|
* How a leased tab navigates. Pure, and exported, because the decision in it is worth a test while
|
|
11
24
|
* the rest of this file needs a real Chromium.
|
|
@@ -11,6 +11,22 @@ import { getSessionMetrics } from '../../telemetry/session-metrics.js';
|
|
|
11
11
|
import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
|
|
12
12
|
import { chromiumInstallCommand, chromiumInstallDepsCommand, bundledPlaywrightVersion, } from '../../command/cli/doctor/browser/chromium-hint.js';
|
|
13
13
|
import { installNetworkMocks } from '../input/network-mock.js';
|
|
14
|
+
/** Reticle's own panel, kept out of a picture of the app: it is ours, not what the app shows. */
|
|
15
|
+
export const HIDE_RETICLE_CHROME_CSS = '[data-reticle-overlay]{display:none !important}';
|
|
16
|
+
/**
|
|
17
|
+
* How Playwright captures a leased tab. A live picture (JPEG) leaves Reticle's panel out, as the
|
|
18
|
+
* visual captures do; a plain PNG capture is unchanged, so no existing baseline moves.
|
|
19
|
+
*/
|
|
20
|
+
export function screenshotOptions(opts) {
|
|
21
|
+
return opts.jpegQuality === undefined
|
|
22
|
+
? { fullPage: true === opts.fullPage }
|
|
23
|
+
: {
|
|
24
|
+
fullPage: true === opts.fullPage,
|
|
25
|
+
type: 'jpeg',
|
|
26
|
+
quality: opts.jpegQuality,
|
|
27
|
+
style: HIDE_RETICLE_CHROME_CSS,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
14
30
|
/**
|
|
15
31
|
* How a leased tab navigates. Pure, and exported, because the decision in it is worth a test while
|
|
16
32
|
* the rest of this file needs a real Chromium.
|
|
@@ -43,7 +59,7 @@ function wrapBrowser(browser) {
|
|
|
43
59
|
close: () => page.close(),
|
|
44
60
|
evaluate: (script) => page.evaluate(script),
|
|
45
61
|
// Playwright returns a Buffer; Uint8Array is what the visual store and differ take.
|
|
46
|
-
screenshot: async (opts) => new Uint8Array(await page.screenshot(
|
|
62
|
+
screenshot: async (opts) => new Uint8Array(await page.screenshot(screenshotOptions(opts ?? {}))),
|
|
47
63
|
// Three moves, same as performGesture: a single move to the center can be a no-op if
|
|
48
64
|
// the pointer was already there, and CSS :hover needs a native hit-test to apply.
|
|
49
65
|
hover: async (x, y) => {
|
|
@@ -20,14 +20,12 @@ export interface PooledPage {
|
|
|
20
20
|
*/
|
|
21
21
|
onConsole?(handler: (text: string) => void): void;
|
|
22
22
|
/**
|
|
23
|
-
* Capture the page as a PNG. OPTIONAL, like `onConsole`: a fake that does not implement it makes
|
|
23
|
+
* Capture the page as a PNG, or a JPEG at `jpegQuality` (a live picture, where size matters more). OPTIONAL, like `onConsole`: a fake that does not implement it makes
|
|
24
24
|
* the pool report "no provider", which is the correct degradation — an absent screenshotter must
|
|
25
25
|
* read as "this context cannot be captured", never as a blank image that a visual diff would then
|
|
26
26
|
* compare against and pass.
|
|
27
27
|
*/
|
|
28
|
-
screenshot?(opts?:
|
|
29
|
-
fullPage?: boolean;
|
|
30
|
-
}): Promise<Uint8Array>;
|
|
28
|
+
screenshot?(opts?: ScreenshotOptions): Promise<Uint8Array>;
|
|
31
29
|
/**
|
|
32
30
|
* Move the real pointer to (x, y) so CSS `:hover` applies. OPTIONAL, like `screenshot`: a fake
|
|
33
31
|
* that does not implement it makes hover refuse rather than dispatch a synthetic mouseover that
|
|
@@ -116,3 +114,8 @@ export interface PooledBrowser {
|
|
|
116
114
|
}
|
|
117
115
|
/** Produces a freshly launched browser. Injected so tests can supply a fake. */
|
|
118
116
|
export type Launcher = () => Promise<PooledBrowser>;
|
|
117
|
+
/** How a page is captured: the whole page or the viewport, and JPEG at a quality instead of PNG. */
|
|
118
|
+
export interface ScreenshotOptions {
|
|
119
|
+
fullPage?: boolean;
|
|
120
|
+
jpegQuality?: number;
|
|
121
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command a session sends its page, as wire JSON.
|
|
3
|
+
*
|
|
4
|
+
* Marked `by: harness` when the Harness is the one calling, so the HUD can show the person watching
|
|
5
|
+
* which of the two is driving their app. Absent means the connected agent, which is also what an
|
|
6
|
+
* older page that does not know the field assumes.
|
|
7
|
+
*/
|
|
8
|
+
export declare function commandPayload(id: string, sessionId: string, name: string, args: Record<string, unknown>): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CommandActor, MessageKind } from '@reticlehq/core';
|
|
2
|
+
import { currentDrivenBy } from '../../hooks/driven-by.js';
|
|
3
|
+
/**
|
|
4
|
+
* The command a session sends its page, as wire JSON.
|
|
5
|
+
*
|
|
6
|
+
* Marked `by: harness` when the Harness is the one calling, so the HUD can show the person watching
|
|
7
|
+
* which of the two is driving their app. Absent means the connected agent, which is also what an
|
|
8
|
+
* older page that does not know the field assumes.
|
|
9
|
+
*/
|
|
10
|
+
export function commandPayload(id, sessionId, name, args) {
|
|
11
|
+
return JSON.stringify({
|
|
12
|
+
kind: MessageKind.COMMAND,
|
|
13
|
+
id,
|
|
14
|
+
sessionId,
|
|
15
|
+
name,
|
|
16
|
+
args,
|
|
17
|
+
...(currentDrivenBy() === undefined ? {} : { by: CommandActor.HARNESS }),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -16,6 +16,8 @@ export interface DevCommand {
|
|
|
16
16
|
script?: string;
|
|
17
17
|
/** The port the script PINS, when it pins one. Absent means the tool picks — never a guess. */
|
|
18
18
|
port?: number;
|
|
19
|
+
/** The app's directory, when it is not the project root. The caller must run the command there. */
|
|
20
|
+
directory?: string;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* The dev command for the project in `directory`, or undefined when there is not one to be sure of.
|
|
@@ -24,3 +26,13 @@ export interface DevCommand {
|
|
|
24
26
|
* default disk reader.
|
|
25
27
|
*/
|
|
26
28
|
export declare function detectDevCommand(directory: string, read?: (path: string) => string | undefined): DevCommand | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The dev command for the project, looking one level down when the root has none.
|
|
31
|
+
*
|
|
32
|
+
* `reticle init` is run at the repo root, and in a monorepo the app is a directory down — so the
|
|
33
|
+
* daemon's "this project declares no dev script" was false for a root whose `frontend/package.json`
|
|
34
|
+
* held `"dev": "vite"`, and the agent asked a human for a command that was on disk (reticle#1368).
|
|
35
|
+
* init's own workspace listing answers which directories to consider; the root's lockfile picks the
|
|
36
|
+
* package manager, because a workspace package usually has no lockfile of its own.
|
|
37
|
+
*/
|
|
38
|
+
export declare function detectDevCommandInProject(directory: string, read?: (path: string) => string | undefined, listDirs?: (path: string) => readonly string[]): DevCommand | undefined;
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Pure apart from one injected reader, so every case is a unit test rather than a temp directory.
|
|
10
10
|
*/
|
|
11
|
-
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
-
import { desktopLaunch } from '@reticlehq/init';
|
|
13
|
+
import { desktopLaunch, workspacePackageDirs } from '@reticlehq/init';
|
|
14
14
|
/**
|
|
15
15
|
* Script names checked, in order.
|
|
16
16
|
*
|
|
@@ -28,6 +28,12 @@ const DEFAULT_PACKAGE_MANAGER = 'npm';
|
|
|
28
28
|
const PACKAGE_JSON = 'package.json';
|
|
29
29
|
/** `--port 4311` / `--port=4311`, the flag every dev server in this list accepts. */
|
|
30
30
|
const PORT_FLAG = /--port[= ]\s*(\d+)/;
|
|
31
|
+
/**
|
|
32
|
+
* `-p 3005`, the short form Next, Nuxt and Astro accept (reticle#1367).
|
|
33
|
+
*
|
|
34
|
+
* Anchored to a start or a whitespace so the `-p` inside a longer flag (`--pretty`) is not a port.
|
|
35
|
+
*/
|
|
36
|
+
const PORT_SHORT_FLAG = /(?:^|\s)-p\s*(\d+)/;
|
|
31
37
|
/** `PORT=8080 remix dev` — the other half of how a project pins its port. */
|
|
32
38
|
const PORT_ENV = /(?:^|\s)PORT=(\d+)/;
|
|
33
39
|
/** Reads a file, or undefined if it is not there / not readable. */
|
|
@@ -41,7 +47,7 @@ function readTextFile(path) {
|
|
|
41
47
|
}
|
|
42
48
|
/** The port the script pins, when it pins one at all. */
|
|
43
49
|
function pinnedPort(script) {
|
|
44
|
-
const match = PORT_FLAG.exec(script) ?? PORT_ENV.exec(script);
|
|
50
|
+
const match = PORT_FLAG.exec(script) ?? PORT_SHORT_FLAG.exec(script) ?? PORT_ENV.exec(script);
|
|
45
51
|
const digits = match?.[1];
|
|
46
52
|
if (digits === undefined)
|
|
47
53
|
return undefined;
|
|
@@ -68,17 +74,15 @@ function scriptsOf(manifest) {
|
|
|
68
74
|
return {};
|
|
69
75
|
return scripts;
|
|
70
76
|
}
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*/
|
|
77
|
-
|
|
77
|
+
/** The package manager the lockfiles under `directory` identify. Anything else is npm. */
|
|
78
|
+
function packageManager(directory, read) {
|
|
79
|
+
return (LOCKFILES.find(({ file }) => read(join(directory, file)) !== undefined)?.manager ??
|
|
80
|
+
DEFAULT_PACKAGE_MANAGER);
|
|
81
|
+
}
|
|
82
|
+
/** The dev command for the package in `directory`, with the manager chosen by the caller. */
|
|
83
|
+
function devCommandFor(directory, manager, read) {
|
|
78
84
|
const manifest = readManifest(read(join(directory, PACKAGE_JSON)));
|
|
79
85
|
const scripts = scriptsOf(manifest);
|
|
80
|
-
const manager = LOCKFILES.find(({ file }) => read(join(directory, file)) !== undefined)?.manager ??
|
|
81
|
-
DEFAULT_PACKAGE_MANAGER;
|
|
82
86
|
// A desktop shell's `dev` is its renderer's dev server, which no window ever opens onto. init's
|
|
83
87
|
// rule decides which launcher it is, so the command the daemon hands an agent and the one init
|
|
84
88
|
// spawns are the same.
|
|
@@ -105,3 +109,48 @@ export function detectDevCommand(directory, read = readTextFile) {
|
|
|
105
109
|
}
|
|
106
110
|
return undefined;
|
|
107
111
|
}
|
|
112
|
+
/** Directory names under `path`, or none when it cannot be read. */
|
|
113
|
+
function readDirNames(path) {
|
|
114
|
+
try {
|
|
115
|
+
return readdirSync(path, { withFileTypes: true })
|
|
116
|
+
.filter((entry) => entry.isDirectory())
|
|
117
|
+
.map((entry) => entry.name);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The dev command for the project in `directory`, or undefined when there is not one to be sure of.
|
|
125
|
+
*
|
|
126
|
+
* `read` is injected so the whole decision is testable without a filesystem; the daemon uses the
|
|
127
|
+
* default disk reader.
|
|
128
|
+
*/
|
|
129
|
+
export function detectDevCommand(directory, read = readTextFile) {
|
|
130
|
+
return devCommandFor(directory, packageManager(directory, read), read);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The dev command for the project, looking one level down when the root has none.
|
|
134
|
+
*
|
|
135
|
+
* `reticle init` is run at the repo root, and in a monorepo the app is a directory down — so the
|
|
136
|
+
* daemon's "this project declares no dev script" was false for a root whose `frontend/package.json`
|
|
137
|
+
* held `"dev": "vite"`, and the agent asked a human for a command that was on disk (reticle#1368).
|
|
138
|
+
* init's own workspace listing answers which directories to consider; the root's lockfile picks the
|
|
139
|
+
* package manager, because a workspace package usually has no lockfile of its own.
|
|
140
|
+
*/
|
|
141
|
+
export function detectDevCommandInProject(directory, read = readTextFile, listDirs = readDirNames) {
|
|
142
|
+
const manager = packageManager(directory, read);
|
|
143
|
+
const here = devCommandFor(directory, manager, read);
|
|
144
|
+
if (here !== undefined)
|
|
145
|
+
return here;
|
|
146
|
+
const packages = workspacePackageDirs({
|
|
147
|
+
readFile: (path) => read(join(directory, path)) ?? null,
|
|
148
|
+
listDirs: (path) => listDirs(join(directory, path)),
|
|
149
|
+
});
|
|
150
|
+
for (const sub of packages) {
|
|
151
|
+
const found = devCommandFor(join(directory, sub), manager, read);
|
|
152
|
+
if (found !== undefined)
|
|
153
|
+
return { ...found, directory: sub };
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReticleEvent } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* Where the SDK last saw the page heading.
|
|
4
|
+
*
|
|
5
|
+
* The SDK emits a navigation-initiated NET_PENDING while the old document is still alive — the
|
|
6
|
+
* one fact that can outrun the socket close (#1256). This keeps it off `Session.url` deliberately:
|
|
7
|
+
* successor matching and the 5xx probe need the page the tab was ON; this is where it was GOING.
|
|
8
|
+
* Timestamps are session-relative milliseconds, read from the session's injected clock.
|
|
9
|
+
*/
|
|
10
|
+
export declare class DepartureNote {
|
|
11
|
+
#private;
|
|
12
|
+
/** Fold one inbound event into the note. */
|
|
13
|
+
observe(event: ReticleEvent, elapsedMs: number): void;
|
|
14
|
+
/**
|
|
15
|
+
* The navigation target, if it is fresh enough to be this departure's story.
|
|
16
|
+
*
|
|
17
|
+
* A navigation intent from long ago is not this departure: the document survived it — it kept
|
|
18
|
+
* talking — so attributing a later disconnect to it would invent a story. Read at tombstone
|
|
19
|
+
* time, when `elapsedMs` is the disconnect time.
|
|
20
|
+
*/
|
|
21
|
+
read(elapsedMs: number): string | undefined;
|
|
22
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { EventType, NetInitiator } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* How long a navigation-departure note stays attributable to the disconnect that follows it.
|
|
4
|
+
*
|
|
5
|
+
* The socket close follows a real departure within seconds; a note older than this belongs to a
|
|
6
|
+
* navigation the document survived, and blaming a later disconnect on it would invent a story.
|
|
7
|
+
*/
|
|
8
|
+
const DEPARTED_TO_FRESHNESS_MS = 30_000;
|
|
9
|
+
/**
|
|
10
|
+
* Where the SDK last saw the page heading.
|
|
11
|
+
*
|
|
12
|
+
* The SDK emits a navigation-initiated NET_PENDING while the old document is still alive — the
|
|
13
|
+
* one fact that can outrun the socket close (#1256). This keeps it off `Session.url` deliberately:
|
|
14
|
+
* successor matching and the 5xx probe need the page the tab was ON; this is where it was GOING.
|
|
15
|
+
* Timestamps are session-relative milliseconds, read from the session's injected clock.
|
|
16
|
+
*/
|
|
17
|
+
export class DepartureNote {
|
|
18
|
+
#note;
|
|
19
|
+
/** Fold one inbound event into the note. */
|
|
20
|
+
observe(event, elapsedMs) {
|
|
21
|
+
if (event.type !== EventType.NET_PENDING)
|
|
22
|
+
return;
|
|
23
|
+
// Only navigation-initiated pendings count: a fetch that never settled is in-flight work,
|
|
24
|
+
// not a departure.
|
|
25
|
+
if (event.data['initiator'] !== NetInitiator.NAVIGATION)
|
|
26
|
+
return;
|
|
27
|
+
// A download never takes the document away, so it must not explain a later disconnect.
|
|
28
|
+
if (true === event.data['download'])
|
|
29
|
+
return;
|
|
30
|
+
const url = event.data['url'];
|
|
31
|
+
if ('string' !== typeof url || 0 === url.length)
|
|
32
|
+
return;
|
|
33
|
+
this.#note = { url, at: elapsedMs };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The navigation target, if it is fresh enough to be this departure's story.
|
|
37
|
+
*
|
|
38
|
+
* A navigation intent from long ago is not this departure: the document survived it — it kept
|
|
39
|
+
* talking — so attributing a later disconnect to it would invent a story. Read at tombstone
|
|
40
|
+
* time, when `elapsedMs` is the disconnect time.
|
|
41
|
+
*/
|
|
42
|
+
read(elapsedMs) {
|
|
43
|
+
if (this.#note === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
return elapsedMs - this.#note.at <= DEPARTED_TO_FRESHNESS_MS ? this.#note.url : undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -33,6 +33,11 @@ export declare function isSessionReplacedError(error: unknown): boolean;
|
|
|
33
33
|
* `PendingCommands.rejectAll` documents, and a caller that CAN wait decides what it means.
|
|
34
34
|
*/
|
|
35
35
|
export declare const SESSION_DISCONNECTED_REASON = "session disconnected";
|
|
36
|
+
/**
|
|
37
|
+
* The close an agent's `reticle_session { action: "end", disconnect: true }` sends. The page's SDK
|
|
38
|
+
* prints it and stops retrying, so it says how to undo it. A close reason is capped at 123 bytes.
|
|
39
|
+
*/
|
|
40
|
+
export declare const SESSION_PARKED_REASON = "detached by the agent (session end, disconnect) \u2014 restart the daemon to attach this window again";
|
|
36
41
|
/**
|
|
37
42
|
* Is this failure the page's transport going away, rather than an answer from the page?
|
|
38
43
|
*
|
|
@@ -38,6 +38,11 @@ export function isSessionReplacedError(error) {
|
|
|
38
38
|
* `PendingCommands.rejectAll` documents, and a caller that CAN wait decides what it means.
|
|
39
39
|
*/
|
|
40
40
|
export const SESSION_DISCONNECTED_REASON = 'session disconnected';
|
|
41
|
+
/**
|
|
42
|
+
* The close an agent's `reticle_session { action: "end", disconnect: true }` sends. The page's SDK
|
|
43
|
+
* prints it and stops retrying, so it says how to undo it. A close reason is capped at 123 bytes.
|
|
44
|
+
*/
|
|
45
|
+
export const SESSION_PARKED_REASON = 'detached by the agent (session end, disconnect) — restart the daemon to attach this window again';
|
|
41
46
|
/**
|
|
42
47
|
* Is this failure the page's transport going away, rather than an answer from the page?
|
|
43
48
|
*
|