@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,17 +10,19 @@
|
|
|
10
10
|
* daemon that outlives the agent by hours has no business scanning ports it does not need.
|
|
11
11
|
*/
|
|
12
12
|
import { probeRouteStatus } from './dev-server/route-status-probe.js';
|
|
13
|
-
import {
|
|
13
|
+
import { probeDevServerStates } from './dev-server/dev-server-probe.js';
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
14
15
|
import { homedir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
15
17
|
import { registeredElsewhere } from '../../memory/recall/registered-projects.js';
|
|
16
18
|
import { explainNoSession } from './no-session-diagnosis.js';
|
|
17
|
-
import {
|
|
19
|
+
import { detectDevCommandInProject } from './dev-server/dev-command.js';
|
|
18
20
|
import { nextActionFor, renderNextAction } from './no-session-next-action.js';
|
|
19
|
-
import { readProjectFramework, readProjectId, readProjectIsDesktop, readProjectPort, } from '../../command/cli/ports/resolve/cli-port.js';
|
|
21
|
+
import { DEV_SERVER_PORTS, readProjectFramework, readProjectId, readProjectIsDesktop, readProjectPort, } from '../../command/cli/ports/resolve/cli-port.js';
|
|
20
22
|
import { discoverProjectConfigs } from '../../command/cli/config/config-discovery.js';
|
|
21
23
|
import { hasProjectConnectedBefore, rememberConnected, } from '../../memory/recall/prior/connection-memory.js';
|
|
22
24
|
import { isAlive, reticleStateHome } from '../../command/daemon/daemon.js';
|
|
23
|
-
import { daemonsServingProjectElsewhere, splitBrainNote } from '../../command/daemon/daemon-resolve.js';
|
|
25
|
+
import { daemonsServingProjectElsewhere, projectDaemonsElsewhere, projectlessNote, splitBrainNote, } from '../../command/daemon/daemon-resolve.js';
|
|
24
26
|
import { stallUptime } from './stall-clock.js';
|
|
25
27
|
import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
|
|
26
28
|
import { findOccupiedSiblings } from '../../command/cli/ports/sibling-ports.js';
|
|
@@ -47,6 +49,19 @@ function attachFailureClause(port, reason) {
|
|
|
47
49
|
'rather than treating this as a problem with the app.'
|
|
48
50
|
: ''));
|
|
49
51
|
}
|
|
52
|
+
/** The port a tombstone URL names, when it names one — anything unparseable names no port. */
|
|
53
|
+
function portOfUrl(url) {
|
|
54
|
+
try {
|
|
55
|
+
const raw = new URL(url).port;
|
|
56
|
+
if ('' === raw)
|
|
57
|
+
return undefined;
|
|
58
|
+
const port = Number(raw);
|
|
59
|
+
return Number.isSafeInteger(port) && port > 0 ? port : undefined;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
50
65
|
/**
|
|
51
66
|
* Start the watch. Returns a stop function; the timer is unref'd so it never holds the daemon up.
|
|
52
67
|
*
|
|
@@ -54,7 +69,6 @@ function attachFailureClause(port, reason) {
|
|
|
54
69
|
* through `wireSessionScope`.
|
|
55
70
|
*/
|
|
56
71
|
export function startNoSessionWatch(options) {
|
|
57
|
-
const probe = options.probe ?? (() => probeDevServers());
|
|
58
72
|
let listening = [];
|
|
59
73
|
/**
|
|
60
74
|
* Ports that accepted a connection and then answered nothing in time.
|
|
@@ -81,6 +95,7 @@ export function startNoSessionWatch(options) {
|
|
|
81
95
|
/** Set only when an attempt actually failed, so the diagnosis can say so instead of hiding it. */
|
|
82
96
|
let attachFailure;
|
|
83
97
|
const directory = options.directory ?? process.cwd();
|
|
98
|
+
const exists = options.exists ?? ((file) => existsSync(join(directory, file)));
|
|
84
99
|
// The boot answer still counts (it is what the daemon scoped its sessions with), but `.reticle.json`
|
|
85
100
|
// is routinely written by `init` AFTER this daemon started, so re-read rather than cache. See the
|
|
86
101
|
// `initialized` comment below, which this shares.
|
|
@@ -146,6 +161,9 @@ export function startNoSessionWatch(options) {
|
|
|
146
161
|
*/
|
|
147
162
|
const splitBrain = () => {
|
|
148
163
|
const projectId = readProjectId(directory);
|
|
164
|
+
if (projectId === undefined) {
|
|
165
|
+
return projectlessNote(options.port, projectDaemonsElsewhere(options.port, stateDir, isAlive));
|
|
166
|
+
}
|
|
149
167
|
return splitBrainNote(options.port, daemonsServingProjectElsewhere(projectId, options.port, stateDir, isAlive, (port) => hasProjectConnectedBefore(stateDir, port, projectId)));
|
|
150
168
|
};
|
|
151
169
|
// Every path that registers a session goes through SessionManager.add, so this is the one hook
|
|
@@ -172,6 +190,29 @@ export function startNoSessionWatch(options) {
|
|
|
172
190
|
projectId: readProjectId(directory),
|
|
173
191
|
root: directory,
|
|
174
192
|
}).map((entry) => entry.port));
|
|
193
|
+
/**
|
|
194
|
+
* The ports the background scan must cover: the well-known list plus the facts that name THIS
|
|
195
|
+
* repo's port without guessing it (#1367).
|
|
196
|
+
*
|
|
197
|
+
* The scan is machine-wide, so it knows 3000/5173/8080… and nothing about this checkout: with a
|
|
198
|
+
* Next app already running on :3005 (`next dev -p 3005`) the diagnosis said nothing was listening
|
|
199
|
+
* and named a command that would have started a duplicate. Three facts name the port instead —
|
|
200
|
+
* the project's own dev script pins it in its own text, the build plugins announced theirs
|
|
201
|
+
* (already trusted for auto-attach), and the last session was on one.
|
|
202
|
+
*/
|
|
203
|
+
const scanPorts = () => {
|
|
204
|
+
const ports = new Set(DEV_SERVER_PORTS);
|
|
205
|
+
for (const port of ownDevServerPorts())
|
|
206
|
+
ports.add(port);
|
|
207
|
+
const pinned = detectDevCommandInProject(directory)?.port;
|
|
208
|
+
if (pinned !== undefined)
|
|
209
|
+
ports.add(pinned);
|
|
210
|
+
const known = options.sessions.lastKnown?.();
|
|
211
|
+
const knownPort = known === undefined ? undefined : portOfUrl(known.url);
|
|
212
|
+
if (knownPort !== undefined)
|
|
213
|
+
ports.add(knownPort);
|
|
214
|
+
return [...ports];
|
|
215
|
+
};
|
|
175
216
|
/**
|
|
176
217
|
* Is `port` demonstrably this project's app? Either its dev server announced it, or this
|
|
177
218
|
* project's own last session was on it. Anything else is a guess, and a guess that opens a
|
|
@@ -218,12 +259,13 @@ export function startNoSessionWatch(options) {
|
|
|
218
259
|
if (running || options.sessions.count() > 0)
|
|
219
260
|
return;
|
|
220
261
|
running = true;
|
|
262
|
+
const candidates = scanPorts();
|
|
221
263
|
void (options.probe === undefined
|
|
222
|
-
? probeDevServerStates().then((states) => {
|
|
264
|
+
? probeDevServerStates(candidates).then((states) => {
|
|
223
265
|
slowListeners = states.slow;
|
|
224
266
|
return states.serving;
|
|
225
267
|
})
|
|
226
|
-
: probe())
|
|
268
|
+
: options.probe(candidates))
|
|
227
269
|
.then(async (ports) => {
|
|
228
270
|
listening = ports;
|
|
229
271
|
siblingListeners =
|
|
@@ -294,6 +336,7 @@ export function startNoSessionWatch(options) {
|
|
|
294
336
|
initialized: scope.initialized,
|
|
295
337
|
...(scope.configsElsewhere === undefined ? {} : { configsElsewhere: scope.configsElsewhere }),
|
|
296
338
|
previouslyConnected: connectedBefore(),
|
|
339
|
+
exists,
|
|
297
340
|
// Read when asked, like every other fact here: a page can dial at any moment, and a daemon
|
|
298
341
|
// that cached "nothing has been refused" at boot would keep saying so.
|
|
299
342
|
authRefused: lastCloseWasAuthFailure(),
|
|
@@ -307,11 +350,16 @@ export function startNoSessionWatch(options) {
|
|
|
307
350
|
// command and the sentence beside it name the same page.
|
|
308
351
|
...(() => {
|
|
309
352
|
const known = options.sessions.lastKnown?.();
|
|
310
|
-
|
|
353
|
+
if (known === undefined)
|
|
354
|
+
return {};
|
|
355
|
+
return {
|
|
356
|
+
lastKnownUrl: known.url,
|
|
357
|
+
...(known.departedTo === undefined ? {} : { departedTo: known.departedTo }),
|
|
358
|
+
};
|
|
311
359
|
})(),
|
|
312
360
|
// Read when asked, like everything else here: a `package.json` can gain a dev script, and a
|
|
313
361
|
// daemon that cached "there is none" at boot would keep saying so for the rest of the day.
|
|
314
|
-
dev:
|
|
362
|
+
dev: detectDevCommandInProject(directory),
|
|
315
363
|
});
|
|
316
364
|
};
|
|
317
365
|
options.sessions.setNoSessionNextAction(() => nextAction(projectScopeFacts()));
|
|
@@ -375,7 +423,12 @@ export function startNoSessionWatch(options) {
|
|
|
375
423
|
const status = lastKnownStatus !== undefined && lastKnownStatus.url === known.url
|
|
376
424
|
? { lastKnownStatus: lastKnownStatus.status }
|
|
377
425
|
: {};
|
|
378
|
-
|
|
426
|
+
// Where the tab was seen heading, when the SDK reported it fresh. Kept separate from
|
|
427
|
+
// lastKnownUrl: the 5xx probe needs the page the tab was ON.
|
|
428
|
+
const departed = known.departedTo === undefined || '' === known.departedTo
|
|
429
|
+
? {}
|
|
430
|
+
: { departedTo: known.departedTo };
|
|
431
|
+
return { lastKnownUrl: known.url, ...status, ...departed };
|
|
379
432
|
})(),
|
|
380
433
|
// How long this daemon has been waiting with no app. The diagnosis uses it to surface
|
|
381
434
|
// "install never finished" — the same condition telemetry already knows about.
|
|
@@ -19,4 +19,7 @@ export function mirroredNarration(sessionId, text) {
|
|
|
19
19
|
export const MIRRORED_COMMANDS = new Set([
|
|
20
20
|
ReticleCommand.NARRATE,
|
|
21
21
|
ReticleCommand.IMPACT,
|
|
22
|
+
// A replay usually reloads the page, so its progress has to reach whichever tab is there now.
|
|
23
|
+
ReticleCommand.FLOW_PROGRESS,
|
|
24
|
+
ReticleCommand.PLAN,
|
|
22
25
|
]);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which directory the dev server that served a page runs in.
|
|
3
|
+
*
|
|
4
|
+
* The tie-breaker for a project several checkouts declare: every git worktree carries the same
|
|
5
|
+
* committed `.reticle.json`, so the projectId cannot tell them apart, but the process serving the
|
|
6
|
+
* page runs inside exactly one of them. Lives with the session rather than the artifact resolver
|
|
7
|
+
* because both observations are about this machine's processes -- daemon state files and `lsof` --
|
|
8
|
+
* which `memory/project` has no business knowing. The resolver takes the answer as an injected
|
|
9
|
+
* function; the composition roots pass this one.
|
|
10
|
+
*/
|
|
11
|
+
import { type DevServerEntry } from '@reticlehq/core';
|
|
12
|
+
/** Where `servingDirectoryOf` gets its two observations. Injected so a test needs no processes. */
|
|
13
|
+
export interface ServingDirectoryIo {
|
|
14
|
+
/** Live dev servers that announced themselves (the Vite plugin writes `devserver-<port>.json`). */
|
|
15
|
+
devServers: () => readonly DevServerEntry[];
|
|
16
|
+
/** The working directory of whatever listens on `port`, or null when it cannot be read. */
|
|
17
|
+
listenerCwd: (port: number) => string | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The directory of the dev server that served `origin`, or undefined when it cannot be observed.
|
|
21
|
+
*
|
|
22
|
+
* Two observations, exact one first. A dev server whose plugin announced this port names its own
|
|
23
|
+
* root and works on every platform. Otherwise the process listening on the port is asked for its
|
|
24
|
+
* working directory, which covers frameworks with no announcing plugin (Next.js) wherever `lsof`
|
|
25
|
+
* runs. Loopback only: a remote page's port says nothing about any process on this machine.
|
|
26
|
+
*/
|
|
27
|
+
export declare function servingDirectoryOf(origin: string, io?: ServingDirectoryIo): string | undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which directory the dev server that served a page runs in.
|
|
3
|
+
*
|
|
4
|
+
* The tie-breaker for a project several checkouts declare: every git worktree carries the same
|
|
5
|
+
* committed `.reticle.json`, so the projectId cannot tell them apart, but the process serving the
|
|
6
|
+
* page runs inside exactly one of them. Lives with the session rather than the artifact resolver
|
|
7
|
+
* because both observations are about this machine's processes -- daemon state files and `lsof` --
|
|
8
|
+
* which `memory/project` has no business knowing. The resolver takes the answer as an injected
|
|
9
|
+
* function; the composition roots pass this one.
|
|
10
|
+
*/
|
|
11
|
+
import { isLoopbackHostname } from '@reticlehq/core';
|
|
12
|
+
import { captureLookup, findListenerCwd } from '../../command/cli/ports/port-holder.js';
|
|
13
|
+
import { reticleStateHome } from '../../command/daemon/daemon.js';
|
|
14
|
+
import { readDevServers } from '../../command/daemon/dev-servers.js';
|
|
15
|
+
const DEFAULT_PORT = { 'http:': 80, 'https:': 443 };
|
|
16
|
+
const liveIo = {
|
|
17
|
+
devServers: () => readDevServers(reticleStateHome()),
|
|
18
|
+
listenerCwd: (port) => findListenerCwd(port, captureLookup),
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The directory of the dev server that served `origin`, or undefined when it cannot be observed.
|
|
22
|
+
*
|
|
23
|
+
* Two observations, exact one first. A dev server whose plugin announced this port names its own
|
|
24
|
+
* root and works on every platform. Otherwise the process listening on the port is asked for its
|
|
25
|
+
* working directory, which covers frameworks with no announcing plugin (Next.js) wherever `lsof`
|
|
26
|
+
* runs. Loopback only: a remote page's port says nothing about any process on this machine.
|
|
27
|
+
*/
|
|
28
|
+
export function servingDirectoryOf(origin, io = liveIo) {
|
|
29
|
+
let url;
|
|
30
|
+
try {
|
|
31
|
+
url = new URL(origin);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (!isLoopbackHostname(url.hostname))
|
|
37
|
+
return undefined;
|
|
38
|
+
const port = '' === url.port ? DEFAULT_PORT[url.protocol] : Number(url.port);
|
|
39
|
+
if (port === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
const announced = io.devServers().find((entry) => entry.port === port);
|
|
42
|
+
if (announced !== undefined)
|
|
43
|
+
return announced.root;
|
|
44
|
+
return io.listenerCwd(port) ?? undefined;
|
|
45
|
+
}
|
|
@@ -163,6 +163,13 @@ interface HealthReport {
|
|
|
163
163
|
* closed list is that an unbounded string can never reach telemetry through it.
|
|
164
164
|
*/
|
|
165
165
|
brand: BrowserBrand | undefined;
|
|
166
|
+
/** Only the keys the page reported, so merging it never erases one an earlier report set. */
|
|
167
|
+
page: PageIdentity;
|
|
168
|
+
}
|
|
169
|
+
/** Which desktop window a session is, and what its document was served with. */
|
|
170
|
+
export interface PageIdentity {
|
|
171
|
+
windowLabel?: string;
|
|
172
|
+
documentStatus?: number;
|
|
166
173
|
}
|
|
167
174
|
/**
|
|
168
175
|
* Narrow a PAGE_HEALTH payload. Every field is optional on the wire — an older SDK does not report
|
|
@@ -161,6 +161,12 @@ export function readHealthEvent(data) {
|
|
|
161
161
|
runtime: 'string' === typeof data['runtime'] ? data['runtime'] : undefined,
|
|
162
162
|
engine: 'string' === typeof data['engine'] ? data['engine'] : undefined,
|
|
163
163
|
brand: Object.values(BrowserBrand).find((known) => known === data['brand']),
|
|
164
|
+
page: {
|
|
165
|
+
...('string' === typeof data['windowLabel'] ? { windowLabel: data['windowLabel'] } : {}),
|
|
166
|
+
...(Number.isInteger(data['documentStatus'])
|
|
167
|
+
? { documentStatus: data['documentStatus'] }
|
|
168
|
+
: {}),
|
|
169
|
+
},
|
|
164
170
|
};
|
|
165
171
|
}
|
|
166
172
|
/**
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { SessionHealth } from './session-health.js';
|
|
1
|
+
import type { PageIdentity, SessionHealth } from './session-health.js';
|
|
2
|
+
/** A document served as 4xx/5xx — a dev server's 404 page that still mounted the SDK, say. */
|
|
3
|
+
export declare function isErrorDocument(page: PageIdentity | undefined): boolean;
|
|
2
4
|
export interface SessionInfo {
|
|
3
5
|
sessionId: string;
|
|
4
6
|
url: string;
|
|
@@ -17,6 +19,10 @@ export interface SessionInfo {
|
|
|
17
19
|
* not render like it.
|
|
18
20
|
*/
|
|
19
21
|
runtime?: string;
|
|
22
|
+
/** The desktop window's own name (Tauri's label: `main`, `setup`) — absent on a web page. */
|
|
23
|
+
window?: string;
|
|
24
|
+
/** Present only when the document was served as an error (4xx/5xx): a 404 page is not the app. */
|
|
25
|
+
documentStatus?: number;
|
|
20
26
|
/** Present only when the page's SDK version differs from the daemon's — see version-skew.ts. */
|
|
21
27
|
versionSkew?: string;
|
|
22
28
|
/** ms since the SDK last reported anything (silence ⇒ likely throttled). */
|
|
@@ -72,6 +78,7 @@ interface SessionView {
|
|
|
72
78
|
runtime: string | undefined;
|
|
73
79
|
versionSkew: string | undefined;
|
|
74
80
|
hidden: boolean;
|
|
81
|
+
page?: PageIdentity;
|
|
75
82
|
health: () => SessionHealth;
|
|
76
83
|
staleMs: () => number;
|
|
77
84
|
pendingMarkCount: () => number;
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* it, and separating them keeps the wire-facing shape reviewable on its own.
|
|
7
7
|
*/
|
|
8
8
|
import { SESSION_LEASE } from '@reticlehq/core';
|
|
9
|
+
/** A document served as 4xx/5xx — a dev server's 404 page that still mounted the SDK, say. */
|
|
10
|
+
export function isErrorDocument(page) {
|
|
11
|
+
return (page?.documentStatus ?? 0) >= 400;
|
|
12
|
+
}
|
|
9
13
|
/**
|
|
10
14
|
* Project a live Session into the shape the tools return.
|
|
11
15
|
*
|
|
@@ -27,6 +31,8 @@ export function buildSessionInfo(session) {
|
|
|
27
31
|
hasCapabilities: session.hasCapabilities,
|
|
28
32
|
// Omitted when the page never said, so absence stays readable as "unknown" rather than "web".
|
|
29
33
|
...(session.runtime === undefined ? {} : { runtime: session.runtime }),
|
|
34
|
+
...(session.page?.windowLabel === undefined ? {} : { window: session.page.windowLabel }),
|
|
35
|
+
...(isErrorDocument(session.page) ? { documentStatus: session.page?.documentStatus ?? 0 } : {}),
|
|
30
36
|
// On every listing, not buried in a log — skew explains failures that read as app bugs.
|
|
31
37
|
...(session.versionSkew === undefined ? {} : { versionSkew: session.versionSkew }),
|
|
32
38
|
hidden: session.hidden,
|
|
@@ -14,6 +14,17 @@ interface ResolveScope {
|
|
|
14
14
|
}
|
|
15
15
|
/** The scheme://host:port of a URL, or undefined if it can't be parsed. Used to compare origins. */
|
|
16
16
|
export declare function originOf(url: string | undefined): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Honest, scoped error when sessions exist but none match the agent's project.
|
|
19
|
+
*
|
|
20
|
+
* The daemon takes its default scope from the `.reticle.json` of whichever directory started it, so
|
|
21
|
+
* attaching from a second project — or another worktree of the SAME project — makes every tool
|
|
22
|
+
* refuse while `reticle_sessions` shows the tab sitting right there. The old message asked "is that
|
|
23
|
+
* app running with @reticlehq/core enabled?", which is the one thing that is definitely true, so the
|
|
24
|
+
* reader goes looking at their app instead of at the scope. Name what IS connected and how to
|
|
25
|
+
* target it: both facts are already in hand here.
|
|
26
|
+
*/
|
|
27
|
+
export declare function scopeMissError(connected: Session[], scope?: ResolveScope): string;
|
|
17
28
|
/** What a refused hello said about itself: the page it was on and the project it claimed. */
|
|
18
29
|
export interface RefusedPage {
|
|
19
30
|
readonly url?: string;
|
|
@@ -31,6 +42,16 @@ export declare class SessionManager {
|
|
|
31
42
|
setDefaultScope(scope: ResolveScope | undefined): void;
|
|
32
43
|
setConnectionRecorder(record: ((projectId: string | undefined) => void) | undefined): void;
|
|
33
44
|
add(session: Session): Session | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Close a window's connection and refuse its id until the daemon restarts.
|
|
47
|
+
*
|
|
48
|
+
* Reported from a Tauri app whose hidden windows kept reconnecting: ending the session only ended
|
|
49
|
+
* the presenter, so the window came straight back. A reload keeps its id (sessionStorage), so the
|
|
50
|
+
* id is what stays parked; the 1008 close tells the SDK not to retry. Its tombstone goes too, so a
|
|
51
|
+
* call naming it is refused rather than handed a sibling window.
|
|
52
|
+
*/
|
|
53
|
+
park(session: Session): void;
|
|
54
|
+
isParked(sessionId: string): boolean;
|
|
34
55
|
remove(session: Session): boolean;
|
|
35
56
|
get(sessionId: string): Session | undefined;
|
|
36
57
|
/**
|
|
@@ -4,7 +4,8 @@ import { declareDrivenRedactionKeys, forgetDrivenRedactionKeys, } from '../input
|
|
|
4
4
|
import { Session } from './session.js';
|
|
5
5
|
import { AttachmentHistory } from './presence/attachment-history.js';
|
|
6
6
|
import { openedBeforeDeparture, pickDocumentSuccessor, } from './session-successor.js';
|
|
7
|
-
import { SESSION_DISCONNECTED_REASON } from './facts/session-replaced.js';
|
|
7
|
+
import { SESSION_DISCONNECTED_REASON, SESSION_PARKED_REASON } from './facts/session-replaced.js';
|
|
8
|
+
import { isErrorDocument } from './session-info.js';
|
|
8
9
|
/** The scheme://host:port of a URL, or undefined if it can't be parsed. Used to compare origins. */
|
|
9
10
|
export function originOf(url) {
|
|
10
11
|
if (url === undefined)
|
|
@@ -43,19 +44,31 @@ function scopeSessions(sessions, scope) {
|
|
|
43
44
|
* reader goes looking at their app instead of at the scope. Name what IS connected and how to
|
|
44
45
|
* target it: both facts are already in hand here.
|
|
45
46
|
*/
|
|
46
|
-
function scopeMissError(connected, scope) {
|
|
47
|
+
export function scopeMissError(connected, scope) {
|
|
47
48
|
const who = scope?.projectId !== undefined
|
|
48
49
|
? `project '${scope.projectId}'`
|
|
49
50
|
: scope?.url !== undefined
|
|
50
51
|
? `your app at ${scope.url}`
|
|
51
52
|
: 'the active project';
|
|
53
|
+
const anyUntagged = connected.some((s) => s.projectId === undefined);
|
|
54
|
+
const anyTagged = connected.some((s) => s.projectId !== undefined);
|
|
55
|
+
const how = anyUntagged && !anyTagged
|
|
56
|
+
? 'with no projectId'
|
|
57
|
+
: anyUntagged && anyTagged
|
|
58
|
+
? 'under a different project or with no projectId'
|
|
59
|
+
: 'under a different project';
|
|
52
60
|
const listed = connected
|
|
53
|
-
.map((s) =>
|
|
61
|
+
.map((s) => s.projectId !== undefined
|
|
62
|
+
? `'${s.projectId}' (${s.url}, sessionId '${s.id}')`
|
|
63
|
+
: `(no projectId: the page's connect() carries none) (${s.url}, sessionId '${s.id}')`)
|
|
54
64
|
.join(', ');
|
|
55
|
-
|
|
56
|
-
`
|
|
57
|
-
|
|
58
|
-
`or restart the daemon from that app's directory
|
|
65
|
+
const advice = anyUntagged
|
|
66
|
+
? `Pass the sessionId above to target one, add a projectId to the app's connect() call or ` +
|
|
67
|
+
`.reticle.json, or restart the daemon from that app's directory.`
|
|
68
|
+
: `Pass the sessionId above to target one, or restart the daemon from that app's directory.`;
|
|
69
|
+
return (`no browser session for ${who}, but ${String(connected.length)} session(s) ARE connected ${how}: ` +
|
|
70
|
+
`${listed}. The daemon scopes to the .reticle.json of the directory it was started in, so ` +
|
|
71
|
+
`this is a scope mismatch, not a dead app. ${advice}`);
|
|
59
72
|
}
|
|
60
73
|
/**
|
|
61
74
|
* Owns the set of connected browser sessions and the smart auto-selection that resolves which one a
|
|
@@ -144,6 +157,24 @@ export class SessionManager {
|
|
|
144
157
|
const scope = driven.projectId === undefined ? { url: driven.url } : { projectId: driven.projectId };
|
|
145
158
|
return scopeSessions([...this.#sessions.values()], scope).filter((s) => s !== driven);
|
|
146
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Close a window's connection and refuse its id until the daemon restarts.
|
|
162
|
+
*
|
|
163
|
+
* Reported from a Tauri app whose hidden windows kept reconnecting: ending the session only ended
|
|
164
|
+
* the presenter, so the window came straight back. A reload keeps its id (sessionStorage), so the
|
|
165
|
+
* id is what stays parked; the 1008 close tells the SDK not to retry. Its tombstone goes too, so a
|
|
166
|
+
* call naming it is refused rather than handed a sibling window.
|
|
167
|
+
*/
|
|
168
|
+
park(session) {
|
|
169
|
+
this.#parked.add(session.id);
|
|
170
|
+
session.disconnect(SESSION_PARKED_REASON);
|
|
171
|
+
this.remove(session);
|
|
172
|
+
this.#tombstones.delete(session.id);
|
|
173
|
+
}
|
|
174
|
+
isParked(sessionId) {
|
|
175
|
+
return this.#parked.has(sessionId);
|
|
176
|
+
}
|
|
177
|
+
#parked = new Set();
|
|
147
178
|
remove(session) {
|
|
148
179
|
if (this.#sessions.get(session.id) !== session)
|
|
149
180
|
return false;
|
|
@@ -164,10 +195,13 @@ export class SessionManager {
|
|
|
164
195
|
this.#siblingsAtDeparture.set(session.id, new Set([...this.#sessions.values()]
|
|
165
196
|
.filter((s) => s !== session && openedBeforeDeparture(s, session))
|
|
166
197
|
.map((s) => s.id)));
|
|
198
|
+
const departedTo = session.departedTo();
|
|
167
199
|
this.#tombstones.set(session.id, {
|
|
168
200
|
id: session.id,
|
|
169
201
|
url: session.url,
|
|
170
202
|
...(session.projectId === undefined ? {} : { projectId: session.projectId }),
|
|
203
|
+
// Where the tab was seen heading, kept off `url`: successor matching needs the old origin.
|
|
204
|
+
...(departedTo === undefined ? {} : { departedTo }),
|
|
171
205
|
});
|
|
172
206
|
while (this.#tombstones.size > MAX_TOMBSTONES) {
|
|
173
207
|
const oldest = this.#tombstones.keys().next().value;
|
|
@@ -495,7 +529,9 @@ export class SessionManager {
|
|
|
495
529
|
// every call auto-targeted the one tab guaranteed to time out.
|
|
496
530
|
const scored = all.map((s) => ({
|
|
497
531
|
s,
|
|
498
|
-
|
|
532
|
+
// An error document (a dev server's 404 that still mounted the SDK) ranks below any working
|
|
533
|
+
// page, hidden or not: it is never the app the agent was asked about.
|
|
534
|
+
score: s.unresponsive() ? 3 : isErrorDocument(s.page) ? 2 : s.throttled() ? 1 : 0,
|
|
499
535
|
ms: s.lastSeenMs(),
|
|
500
536
|
}));
|
|
501
537
|
const bestScore = Math.min(...scored.map((x) => x.score));
|
|
@@ -101,7 +101,10 @@ export function endAllSessions(sessions, reason) {
|
|
|
101
101
|
for (const session of sessions.all()) {
|
|
102
102
|
if (session.isEnded())
|
|
103
103
|
continue;
|
|
104
|
-
|
|
104
|
+
// WAITING, not WARN: a dropped MCP connection is far more often an agent that finished its turn
|
|
105
|
+
// than one that crashed, and the daemon cannot tell them apart. Red at the end of every
|
|
106
|
+
// completed Harness or agent run read as an error the run never had.
|
|
107
|
+
endWithUndelivered(session, reason, PresenterTone.WAITING);
|
|
105
108
|
ended.push(session.id);
|
|
106
109
|
}
|
|
107
110
|
if (ended.length > 0)
|
|
@@ -14,6 +14,11 @@ export interface SessionIdentity {
|
|
|
14
14
|
id: string;
|
|
15
15
|
url: string;
|
|
16
16
|
projectId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Where the tab was seen heading just before it went away (#1256). Separate from `url` — the
|
|
19
|
+
* old origin — because successor matching must match on where the tab WAS.
|
|
20
|
+
*/
|
|
21
|
+
departedTo?: string;
|
|
17
22
|
}
|
|
18
23
|
export interface SuccessorClock {
|
|
19
24
|
now: () => number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { ReticleCommand } from '@reticlehq/core';
|
|
2
|
+
import { HudCorner, HudVisibility, ReticleCommand } from '@reticlehq/core';
|
|
3
3
|
import { ReticleTool } from '@reticlehq/core';
|
|
4
4
|
import { asNumber, asString } from '@reticlehq/core';
|
|
5
5
|
import { idleMsSchema } from '../../surface/tools/args/numeric-bounds.js';
|
|
@@ -12,11 +12,17 @@ import { idleMsSchema } from '../../surface/tools/args/numeric-bounds.js';
|
|
|
12
12
|
export const SESSION_TOOLS = [
|
|
13
13
|
{
|
|
14
14
|
name: ReticleTool.SESSION_TUNE,
|
|
15
|
-
description: 'Tune the presenter session for this app. { idleEndMs } sets how long the session stays open after you go quiet before the panel shows the human you are WAITING (your turn). Default 5min — the SLOW backstop; signal handback IMMEDIATELY with reticle_session{action:"yield"} instead of waiting for this. Lower it for snappier auto-handback, raise it for a slow app where long gaps between your tool calls are normal. Enforced SERVER-SIDE (immune to background-tab throttling); it also fires if you (the MCP client) disconnect — so a forgotten or crashed session never leaves the HUD reading "live". Going quiet then acting again revives the session automatically. Returns { applied, idleEndMs }.',
|
|
15
|
+
description: 'Tune the presenter session for this app. { idleEndMs } sets how long the session stays open after you go quiet before the panel shows the human you are WAITING (your turn). Default 5min — the SLOW backstop; signal handback IMMEDIATELY with reticle_session{action:"yield"} instead of waiting for this. Lower it for snappier auto-handback, raise it for a slow app where long gaps between your tool calls are normal. Enforced SERVER-SIDE (immune to background-tab throttling); it also fires if you (the MCP client) disconnect — so a forgotten or crashed session never leaves the HUD reading "live". Going quiet then acting again revives the session automatically. Also moves the HUD out of your way: { hud: "hidden" | "shown" | "removed" } (removed lasts until the page reloads), or a corner to move it to: { hud: "top-left" }. Use it when the HUD covers a control you must test; say so in your report. Returns { applied, idleEndMs, hud }.',
|
|
16
16
|
inputSchema: {
|
|
17
17
|
idleEndMs: idleMsSchema
|
|
18
18
|
.optional()
|
|
19
19
|
.describe('Idle window in milliseconds after which the panel shows WAITING (your turn). Default: 300000 (5 min) — the slow backstop; prefer reticle_session{action:"yield"}. Raise for slow apps.'),
|
|
20
|
+
hud: z
|
|
21
|
+
.enum([
|
|
22
|
+
...Object.values(HudVisibility),
|
|
23
|
+
...Object.values(HudCorner),
|
|
24
|
+
])
|
|
25
|
+
.optional(),
|
|
20
26
|
sessionId: z
|
|
21
27
|
.string()
|
|
22
28
|
.optional()
|
|
@@ -25,6 +31,7 @@ export const SESSION_TOOLS = [
|
|
|
25
31
|
outputSchema: {
|
|
26
32
|
applied: z.boolean(),
|
|
27
33
|
idleEndMs: z.number().optional(),
|
|
34
|
+
hud: z.array(z.string()).optional(),
|
|
28
35
|
},
|
|
29
36
|
handler: async (deps, args) => {
|
|
30
37
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
@@ -33,9 +40,25 @@ export const SESSION_TOOLS = [
|
|
|
33
40
|
// authority), so the agent-set window is honored even when the tab is backgrounded.
|
|
34
41
|
if (idleEndMs !== undefined)
|
|
35
42
|
session.setIdleEndMs(idleEndMs);
|
|
36
|
-
|
|
43
|
+
// One parameter on the wire to the agent, two to the page: a corner moves it, anything else
|
|
44
|
+
// shows, hides or removes it.
|
|
45
|
+
const asked = asString(args['hud']);
|
|
46
|
+
const isCorner = Object.values(HudCorner).includes(asked);
|
|
47
|
+
const hud = isCorner ? undefined : asked;
|
|
48
|
+
const corner = isCorner ? asked : undefined;
|
|
49
|
+
const res = await session.command(ReticleCommand.SESSION_CONFIG, {
|
|
50
|
+
...(idleEndMs === undefined ? {} : { idleEndMs }),
|
|
51
|
+
...(hud === undefined ? {} : { hud }),
|
|
52
|
+
...(corner === undefined ? {} : { corner }),
|
|
53
|
+
});
|
|
37
54
|
if (!res.ok)
|
|
38
55
|
throw new Error(res.error ?? 'session config failed');
|
|
56
|
+
// Asked to move the HUD and the page says nothing about it: an SDK older than this daemon, or
|
|
57
|
+
// no HUD mounted. Reported, never `applied: true`: an agent told the HUD moved would test a
|
|
58
|
+
// control it is still covering.
|
|
59
|
+
const applied = res.result?.hud;
|
|
60
|
+
if ((hud !== undefined || corner !== undefined) && !Array.isArray(applied))
|
|
61
|
+
throw new Error('the page did not apply `hud`: its Reticle SDK predates it, or no HUD is mounted. Update the SDK (npx @reticlehq/server update) or test with the HUD where it is.');
|
|
39
62
|
return res.result ?? { applied: true };
|
|
40
63
|
},
|
|
41
64
|
},
|
|
@@ -5,9 +5,9 @@ import { LastAct } from './last-act.js';
|
|
|
5
5
|
import { GapLedger } from '@reticlehq/engine/evidence/gap-ledger.js';
|
|
6
6
|
import { CaptureLedger } from '../../surface/tools/feature-capture.js';
|
|
7
7
|
import { type PageRuntime } from './timing/command-timeout.js';
|
|
8
|
-
import { type SessionHealth } from './session-health.js';
|
|
8
|
+
import { readHealthEvent, type SessionHealth } from './session-health.js';
|
|
9
9
|
export type { SessionHealth };
|
|
10
|
-
import { PresenterTone, SessionState, type BrowserBrand, type CommandResult, type JournalAction, type JournalWriteLoss, type HelloMessage, type HumanControlData, type ReticleEvent } from '@reticlehq/core';
|
|
10
|
+
import { ReticleCommand, PresenterTone, SessionState, type BrowserBrand, type CommandResult, type JournalAction, type JournalWriteLoss, type HelloMessage, type HumanControlData, type ReticleEvent } from '@reticlehq/core';
|
|
11
11
|
import type { JournalReader, JournalRecorder } from '../../memory/journal/journal-recorder.js';
|
|
12
12
|
import { type EventQueryOptions } from '../../memory/journal/journal-query.js';
|
|
13
13
|
import { type AmbientCounts } from '@reticlehq/engine/window/ambient.js';
|
|
@@ -60,9 +60,12 @@ export declare class Session implements HandshakeFacts {
|
|
|
60
60
|
commands?: readonly string[] | undefined;
|
|
61
61
|
sourceMapping?: boolean | undefined;
|
|
62
62
|
readonly redactKeys: readonly string[];
|
|
63
|
+
/** The desktop window this is and its document's HTTP status, merged from every PAGE_HEALTH. */
|
|
64
|
+
readonly page: ReturnType<typeof readHealthEvent>['page'];
|
|
63
65
|
constructor(hello: HelloMessage, socket: WebSocket, clock: Clock);
|
|
64
66
|
/** Milliseconds since this session connected — the authoritative buffer clock. */
|
|
65
67
|
elapsed(): number;
|
|
68
|
+
departedTo(): string | undefined;
|
|
66
69
|
/** Mark that the SDK was just heard from. Called on every inbound message. */
|
|
67
70
|
touch(): void;
|
|
68
71
|
/** ms since the SDK last reported anything (distinct from elapsed-since-connect). */
|
|
@@ -273,6 +276,8 @@ export declare class Session implements HandshakeFacts {
|
|
|
273
276
|
pushImpact(read: () => ImpactSnapshot | undefined, immediate?: boolean): void;
|
|
274
277
|
pushPresenter(state: SessionState, text?: string, tone?: PresenterTone): void;
|
|
275
278
|
pushNarration(text: string): void;
|
|
279
|
+
/** A HUD replay's progress, or the Harness's drive plan: pictures the HUD redraws as they change. */
|
|
280
|
+
pushView(name: typeof ReticleCommand.FLOW_PROGRESS | typeof ReticleCommand.PLAN, args: object): void;
|
|
276
281
|
/** The one-time lease block on the first agent command, then undefined forever after. */
|
|
277
282
|
takeSessionLease(): SessionLease | undefined;
|
|
278
283
|
/**
|