@reticlehq/server 2.12.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +67 -109
- package/dist/bridge/auth-failure-reason.d.ts +6 -2
- package/dist/bridge/auth-failure-reason.js +15 -8
- package/dist/bridge/bridge.d.ts +18 -1
- package/dist/bridge/bridge.js +247 -164
- package/dist/bridge/pairing-token.d.ts +5 -0
- package/dist/bridge/pairing-token.js +28 -0
- package/dist/bridge/protocol-skew.d.ts +37 -0
- package/dist/bridge/protocol-skew.js +45 -0
- package/dist/capsule/capsule-store.d.ts +6 -6
- package/dist/capsule/causal-summary.js +7 -2
- package/dist/cli/cli-doctor.js +31 -1
- package/dist/cli/cli-launch.d.ts +11 -1
- package/dist/cli/cli-launch.js +59 -10
- package/dist/cli/cli-parse.d.ts +15 -1
- package/dist/cli/cli-parse.js +212 -8
- package/dist/cli/cli-verify.d.ts +1 -0
- package/dist/cli/cli-verify.js +3 -2
- package/dist/cli/cloud-cli.js +428 -259
- package/dist/cli/cloud-keystore.d.ts +37 -0
- package/dist/cli/cloud-keystore.js +97 -0
- package/dist/cli/cloud-kit.d.ts +37 -0
- package/dist/cli/cloud-kit.js +121 -0
- package/dist/cli/cloud-login.d.ts +17 -0
- package/dist/cli/cloud-login.js +183 -0
- package/dist/cli/cloud-session.d.ts +48 -0
- package/dist/cli/cloud-session.js +53 -0
- package/dist/cli/config-discovery.js +1 -0
- package/dist/cli/daemon-start-options.d.ts +32 -0
- package/dist/cli/daemon-start-options.js +38 -0
- package/dist/cli/device-flow.d.ts +76 -0
- package/dist/cli/device-flow.js +56 -0
- package/dist/cli/doctor-rows.d.ts +9 -0
- package/dist/cli/doctor-rows.js +9 -0
- package/dist/cli/drive-attach.d.ts +13 -0
- package/dist/cli/drive-attach.js +18 -0
- package/dist/cli/drive-command.js +7 -3
- package/dist/cli/mcp-command.js +51 -20
- package/dist/cli/project-name.d.ts +30 -0
- package/dist/cli/project-name.js +44 -0
- package/dist/cli/status-next-action.d.ts +14 -0
- package/dist/cli/status-next-action.js +4 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +134 -80
- package/dist/cloud/cloud-config.d.ts +22 -1
- package/dist/cloud/cloud-config.js +64 -5
- package/dist/cloud/cloud-sync.d.ts +5 -5
- package/dist/cloud/cloud-sync.js +33 -2
- package/dist/cloud/sync-cycle.d.ts +110 -0
- package/dist/cloud/sync-cycle.js +270 -0
- package/dist/cloud/sync-daemon.d.ts +69 -0
- package/dist/cloud/sync-daemon.js +260 -0
- package/dist/cloud/sync-disk.d.ts +9 -0
- package/dist/cloud/sync-disk.js +191 -0
- package/dist/cloud/sync-hash.d.ts +2 -0
- package/dist/cloud/sync-hash.js +35 -0
- package/dist/crawl/crawl-tools.js +5 -3
- package/dist/crawl/crawl.d.ts +3 -0
- package/dist/crawl/crawl.js +5 -0
- package/dist/daemon/daemon-resolve.d.ts +43 -0
- package/dist/daemon/daemon-resolve.js +69 -0
- package/dist/daemon/dev-servers.d.ts +15 -0
- package/dist/daemon/dev-servers.js +68 -0
- package/dist/daemon/wake-decision.d.ts +32 -0
- package/dist/daemon/wake-decision.js +38 -0
- package/dist/domain/domain-tools.js +2 -2
- package/dist/events/contradictions.d.ts +18 -0
- package/dist/events/contradictions.js +134 -21
- package/dist/events/declared.d.ts +7 -4
- package/dist/events/declared.js +15 -6
- package/dist/events/echo-mismatch.js +64 -14
- package/dist/events/event-filters.d.ts +8 -0
- package/dist/events/event-filters.js +19 -2
- package/dist/events/predicate-eval.js +53 -5
- package/dist/events/predicate-precheck.d.ts +8 -0
- package/dist/events/predicate-precheck.js +41 -0
- package/dist/events/predicate-route.d.ts +14 -0
- package/dist/events/predicate-route.js +36 -8
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +37 -5
- package/dist/events/split-text-miss.d.ts +30 -0
- package/dist/events/split-text-miss.js +44 -0
- package/dist/flows/flake.d.ts +6 -6
- package/dist/flows/flow-memory-consult.d.ts +55 -0
- package/dist/flows/flow-memory-consult.js +38 -0
- package/dist/flows/flow-replay-run.d.ts +18 -7
- package/dist/flows/flow-replay-run.js +239 -22
- package/dist/flows/flow-replay.js +11 -2
- package/dist/flows/flow-step-runners.d.ts +8 -0
- package/dist/flows/flow-step-runners.js +8 -2
- package/dist/flows/flow-store-for-session.d.ts +31 -0
- package/dist/flows/flow-store-for-session.js +33 -0
- package/dist/flows/flow-tools.js +58 -32
- package/dist/flows/flows.d.ts +8 -0
- package/dist/flows/flows.js +468 -2
- package/dist/flows/heal-run.js +5 -2
- package/dist/flows/predicate-to-expect.d.ts +17 -12
- package/dist/flows/predicate-to-expect.js +30 -12
- package/dist/flows/recordings.d.ts +13 -1
- package/dist/flows/recordings.js +17 -2
- package/dist/flows/replay.d.ts +22 -2
- package/dist/flows/replay.js +28 -2
- package/dist/flows/verify-change-tools.js +5 -2
- package/dist/honesty/blind-spots.d.ts +55 -2
- package/dist/honesty/blind-spots.js +63 -3
- package/dist/honesty/honesty.d.ts +24 -0
- package/dist/honesty/honesty.js +25 -1
- package/dist/honesty/instrumentation-gaps.d.ts +9 -0
- package/dist/honesty/instrumentation-gaps.js +29 -2
- package/dist/honesty/verified.d.ts +2 -1
- package/dist/honesty/verified.js +16 -1
- package/dist/http-server.js +12 -0
- package/dist/impact/impact-recorder.d.ts +31 -8
- package/dist/impact/impact-recorder.js +104 -21
- package/dist/impact/impact-store.d.ts +15 -1
- package/dist/impact/impact-store.js +43 -2
- package/dist/index.js +113 -7
- package/dist/init/agent-rules.d.ts +13 -6
- package/dist/init/agent-rules.js +23 -12
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +28 -0
- package/dist/init/capabilities.d.ts +4 -1
- package/dist/init/capabilities.js +22 -2
- package/dist/init/confirm.d.ts +31 -7
- package/dist/init/confirm.js +48 -8
- package/dist/init/cra.d.ts +20 -4
- package/dist/init/cra.js +57 -17
- package/dist/init/csp-check.js +17 -2
- package/dist/init/csp-doctor.d.ts +8 -1
- package/dist/init/csp-doctor.js +8 -3
- package/dist/init/dev-script.d.ts +3 -1
- package/dist/init/dev-script.js +8 -5
- package/dist/init/format-generated.d.ts +18 -0
- package/dist/init/format-generated.js +39 -0
- package/dist/init/install-hint.d.ts +9 -0
- package/dist/init/install-hint.js +29 -0
- package/dist/init/mcp-clients.d.ts +7 -0
- package/dist/init/mcp-clients.js +7 -0
- package/dist/init/mcp.d.ts +1 -1
- package/dist/init/mcp.js +1 -1
- package/dist/init/node-io.js +11 -1
- package/dist/init/plan-framework.js +81 -45
- package/dist/init/plan.d.ts +20 -7
- package/dist/init/plan.js +20 -45
- package/dist/init/preflight.d.ts +28 -0
- package/dist/init/preflight.js +38 -0
- package/dist/init/run.d.ts +32 -9
- package/dist/init/run.js +141 -119
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/snippets.js +41 -25
- package/dist/init/vite-config.d.ts +1 -1
- package/dist/init/vite-config.js +39 -25
- package/dist/init/workspace-apps.d.ts +17 -10
- package/dist/init/workspace-apps.js +110 -0
- package/dist/input/network-mock-tools.js +3 -6
- package/dist/input/real-input.js +25 -5
- package/dist/input/scroll-tools.js +4 -6
- package/dist/input/viewport-tools.js +5 -4
- package/dist/intent/inline-intent.d.ts +10 -19
- package/dist/intent/inline-intent.js +91 -1
- package/dist/intent/intent-shard-store.d.ts +72 -0
- package/dist/intent/intent-shard-store.js +203 -0
- package/dist/intent/intent-shard.d.ts +411 -0
- package/dist/intent/intent-shard.js +176 -0
- package/dist/intent/intent-store.d.ts +12 -0
- package/dist/intent/intent-store.js +21 -0
- package/dist/intent/intent-subject.d.ts +45 -0
- package/dist/intent/intent-subject.js +177 -0
- package/dist/intent/intent-tools.js +65 -2
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +15 -0
- package/dist/license/license-env.d.ts +29 -0
- package/dist/license/license-env.js +148 -0
- package/dist/loopback-agent.d.ts +31 -0
- package/dist/loopback-agent.js +57 -0
- package/dist/mcp/attach-memory.d.ts +18 -2
- package/dist/mcp/attach-memory.js +50 -7
- package/dist/mcp/client-identity.d.ts +31 -0
- package/dist/mcp/client-identity.js +51 -0
- package/dist/mcp/mcp-post-transport.d.ts +26 -0
- package/dist/mcp/mcp-post-transport.js +109 -0
- package/dist/mcp/mcp-proxy.d.ts +2 -1
- package/dist/mcp/mcp-proxy.js +52 -62
- package/dist/mcp/mcp.js +7 -1
- package/dist/mcp/proxy-backoff.d.ts +11 -0
- package/dist/mcp/proxy-backoff.js +13 -0
- package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
- package/dist/mcp/proxy-daemon-probe.js +7 -1
- package/dist/mcp/proxy-handshake.d.ts +23 -0
- package/dist/mcp/proxy-handshake.js +31 -0
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +12 -0
- package/dist/memory/memory-tools.d.ts +2 -0
- package/dist/memory/memory-tools.js +82 -0
- package/dist/memory/project-memory.d.ts +34 -0
- package/dist/memory/project-memory.js +96 -0
- package/dist/oracles/propose-consequences.js +12 -3
- package/dist/pool/browser-pool.d.ts +63 -4
- package/dist/pool/browser-pool.js +113 -4
- package/dist/pool/playwright-launcher.js +9 -0
- package/dist/project/project-tools.js +3 -3
- package/dist/project/reticle-dir.d.ts +15 -2
- package/dist/project/reticle-dir.js +21 -4
- package/dist/runs/run-store.d.ts +10 -0
- package/dist/runs/run-store.js +10 -0
- package/dist/runs/verification-sync.d.ts +0 -11
- package/dist/runs/verification-sync.js +22 -1
- package/dist/runs/verify-server.d.ts +8 -1
- package/dist/runs/verify-server.js +14 -2
- package/dist/session/command-timeout.d.ts +5 -2
- package/dist/session/command-timeout.js +5 -4
- package/dist/session/lease-visibility.d.ts +67 -0
- package/dist/session/lease-visibility.js +76 -0
- package/dist/session/no-session-diagnosis.d.ts +23 -0
- package/dist/session/no-session-diagnosis.js +58 -17
- package/dist/session/no-session-next-action.d.ts +7 -0
- package/dist/session/no-session-next-action.js +42 -3
- package/dist/session/no-session-watch.d.ts +10 -6
- package/dist/session/no-session-watch.js +51 -19
- package/dist/session/pending-commands.d.ts +11 -1
- package/dist/session/pending-commands.js +12 -2
- package/dist/session/resolve-within.d.ts +34 -0
- package/dist/session/resolve-within.js +53 -0
- package/dist/session/session-info.d.ts +18 -0
- package/dist/session/session-info.js +11 -0
- package/dist/session/session-manager.d.ts +14 -0
- package/dist/session/session-manager.js +61 -3
- package/dist/session/session-mirror.d.ts +16 -0
- package/dist/session/session-mirror.js +22 -0
- package/dist/session/session-recommendation.d.ts +19 -5
- package/dist/session/session-recommendation.js +25 -9
- package/dist/session/session-successor.js +36 -1
- package/dist/session/session-tools.js +2 -2
- package/dist/session/session.d.ts +22 -2
- package/dist/session/session.js +87 -7
- package/dist/session/wall-clock.d.ts +12 -0
- package/dist/session/wall-clock.js +14 -0
- package/dist/setup/agent-configs.d.ts +96 -0
- package/dist/setup/agent-configs.js +333 -0
- package/dist/setup/agent-io.d.ts +9 -0
- package/dist/setup/agent-io.js +16 -0
- package/dist/setup/agent-writer.d.ts +51 -0
- package/dist/setup/agent-writer.js +115 -0
- package/dist/setup/approval-migration.d.ts +47 -0
- package/dist/setup/approval-migration.js +68 -0
- package/dist/setup/auto-approve.d.ts +85 -0
- package/dist/setup/auto-approve.js +150 -0
- package/dist/setup/bridge-port.d.ts +15 -0
- package/dist/setup/bridge-port.js +24 -0
- package/dist/setup/desktop-shape.d.ts +57 -0
- package/dist/setup/desktop-shape.js +63 -0
- package/dist/setup/dev-server-wait.d.ts +60 -0
- package/dist/setup/dev-server-wait.js +65 -0
- package/dist/setup/drive-agent.d.ts +71 -0
- package/dist/setup/drive-agent.js +171 -0
- package/dist/setup/drive-plan.d.ts +68 -0
- package/dist/setup/drive-plan.js +130 -0
- package/dist/setup/ensure-daemon.d.ts +29 -0
- package/dist/setup/ensure-daemon.js +58 -0
- package/dist/setup/init-runtime.d.ts +47 -0
- package/dist/setup/init-runtime.js +160 -0
- package/dist/setup/interrupt.d.ts +25 -0
- package/dist/setup/interrupt.js +40 -0
- package/dist/setup/license-key.d.ts +39 -0
- package/dist/setup/license-key.js +72 -0
- package/dist/setup/listeners.d.ts +37 -0
- package/dist/setup/listeners.js +85 -0
- package/dist/setup/node-effects.d.ts +60 -0
- package/dist/setup/node-effects.js +210 -0
- package/dist/setup/page-probe.d.ts +39 -0
- package/dist/setup/page-probe.js +49 -0
- package/dist/setup/relaunch.d.ts +36 -0
- package/dist/setup/relaunch.js +57 -0
- package/dist/setup/remaining-steps.d.ts +19 -0
- package/dist/setup/remaining-steps.js +31 -0
- package/dist/setup/run-setup.d.ts +99 -0
- package/dist/setup/run-setup.js +187 -0
- package/dist/setup/session-pick.d.ts +47 -0
- package/dist/setup/session-pick.js +51 -0
- package/dist/setup/setup-command.d.ts +45 -0
- package/dist/setup/setup-command.js +199 -0
- package/dist/setup/setup-options.d.ts +74 -0
- package/dist/setup/setup-options.js +67 -0
- package/dist/setup/transcripts.d.ts +16 -0
- package/dist/setup/transcripts.js +43 -0
- package/dist/status-payload.d.ts +14 -1
- package/dist/status-payload.js +23 -1
- package/dist/telemetry/daemon-telemetry.d.ts +8 -1
- package/dist/telemetry/daemon-telemetry.js +52 -5
- package/dist/telemetry/error-class.js +1 -1
- package/dist/telemetry/feedback-context.d.ts +9 -1
- package/dist/telemetry/feedback-context.js +60 -29
- package/dist/telemetry/feedback-local.d.ts +15 -0
- package/dist/telemetry/feedback-local.js +56 -0
- package/dist/telemetry/feedback.d.ts +11 -0
- package/dist/telemetry/feedback.js +19 -6
- package/dist/telemetry/init-telemetry.d.ts +2 -0
- package/dist/telemetry/init-telemetry.js +2 -0
- package/dist/telemetry/install-source.d.ts +14 -0
- package/dist/telemetry/install-source.js +30 -0
- package/dist/telemetry/license-activation.d.ts +7 -1
- package/dist/telemetry/license-activation.js +9 -5
- package/dist/telemetry/mcp-connection.js +7 -0
- package/dist/telemetry/project-profile.d.ts +12 -1
- package/dist/telemetry/project-profile.js +11 -6
- package/dist/telemetry/proxy-telemetry.d.ts +3 -0
- package/dist/telemetry/proxy-telemetry.js +21 -0
- package/dist/telemetry/session-metrics.d.ts +12 -0
- package/dist/telemetry/session-metrics.js +35 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +3 -1
- package/dist/telemetry/tool-refused.js +34 -2
- package/dist/tools/act-capsule.d.ts +10 -0
- package/dist/tools/act-capsule.js +8 -2
- package/dist/tools/act-danger.d.ts +2 -1
- package/dist/tools/act-danger.js +11 -3
- package/dist/tools/act-observation.d.ts +41 -0
- package/dist/tools/act-observation.js +36 -0
- package/dist/tools/act-preflight.d.ts +20 -0
- package/dist/tools/act-preflight.js +98 -0
- package/dist/tools/act-sequence-retry.d.ts +45 -0
- package/dist/tools/act-sequence-retry.js +58 -0
- package/dist/tools/act-sequence-tool.d.ts +13 -0
- package/dist/tools/act-sequence-tool.js +135 -0
- package/dist/tools/act-target.d.ts +14 -0
- package/dist/tools/act-target.js +47 -0
- package/dist/tools/act-tools.d.ts +21 -1
- package/dist/tools/act-tools.js +100 -213
- package/dist/tools/assert-verdict.js +22 -16
- package/dist/tools/browser-tools.js +6 -2
- package/dist/tools/dynamic-tools.js +24 -5
- package/dist/tools/error-recovery.d.ts +1 -0
- package/dist/tools/error-recovery.js +8 -0
- package/dist/tools/invoke-tool.js +59 -9
- package/dist/tools/lease-tools.d.ts +30 -4
- package/dist/tools/lease-tools.js +169 -5
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +22 -0
- package/dist/tools/numeric-bounds.d.ts +100 -0
- package/dist/tools/numeric-bounds.js +125 -0
- package/dist/tools/observe-tools.js +47 -40
- package/dist/tools/read-tools.js +28 -7
- package/dist/tools/real-input-attempt.d.ts +10 -0
- package/dist/tools/real-input-attempt.js +59 -1
- package/dist/tools/reconcile-tools.js +4 -4
- package/dist/tools/tool-kit.d.ts +12 -0
- package/dist/tools/tool-kit.js +6 -1
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tools-helpers.d.ts +19 -0
- package/dist/tools/tools-helpers.js +46 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +56 -8
- package/dist/update/reticle-deps.d.ts +9 -0
- package/dist/update/reticle-deps.js +27 -0
- package/dist/update/sdk-sync.d.ts +1 -2
- package/dist/update/sdk-sync.js +1 -21
- package/dist/update/update-nudge.d.ts +18 -0
- package/dist/update/update-nudge.js +20 -0
- package/dist/version/peer-announce.js +4 -2
- package/dist/version/sdk-fix.d.ts +47 -0
- package/dist/version/sdk-fix.js +127 -0
- package/dist/version/version-nudge.d.ts +14 -0
- package/dist/version/version-nudge.js +21 -0
- package/dist/version/version-skew.d.ts +38 -3
- package/dist/version/version-skew.js +83 -7
- package/dist/visual/visual-store.d.ts +6 -6
- package/dist/visual/visual-store.js +18 -16
- package/dist/visual/visual-tools.js +57 -12
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/serve.mdx +4 -2
- package/docs/cli.mdx +7 -3
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +1 -1
- package/docs/getting-started.md +19 -3
- package/docs/index.mdx +9 -6
- package/docs/install-agentic.mdx +38 -12
- package/docs/install-manual.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/skill-file.mdx +3 -3
- package/docs/telemetry-contract.md +108 -4
- package/docs/tools-act-sequence.mdx +16 -0
- package/docs/usage.md +1 -1
- package/package.json +4 -3
- package/dist/init/cursor.d.ts +0 -24
- package/dist/init/cursor.js +0 -80
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a session, or spend the caller's stated budget waiting for one to arrive.
|
|
3
|
+
*
|
|
4
|
+
* Every tool that takes a `timeout_ms` used to resolve the session as its FIRST statement, so an
|
|
5
|
+
* empty session map threw before the budget was ever consulted. A caller that said "wait up to
|
|
6
|
+
* thirty seconds for the app" was refused in under a millisecond, and the only way to actually wait
|
|
7
|
+
* was to poll `reticle_sessions` blind.
|
|
8
|
+
*
|
|
9
|
+
* That is the wrong answer to the commonest situation in the product. The moment an agent has run
|
|
10
|
+
* `init` and restarted the dev server is exactly the moment there is no session yet and one is about
|
|
11
|
+
* to appear — and `no_session` is the refusal agents hit most. An agent that gets refused rarely
|
|
12
|
+
* retries; it concludes and moves on. Spending the budget it already asked for turns the most common
|
|
13
|
+
* dead end into a wait that ends in a verdict.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately narrow in two ways.
|
|
16
|
+
*
|
|
17
|
+
* Only the EMPTY case waits. A `sessionId` that names a session which is not connected while others
|
|
18
|
+
* are is a different error with a different fix, and waiting on it would hide a typo behind a
|
|
19
|
+
* timeout.
|
|
20
|
+
*
|
|
21
|
+
* The original error is what gets thrown on expiry, never a generic timeout message. That error
|
|
22
|
+
* carries the daemon's whole diagnosis — the port scan, the next action, the literal command — and
|
|
23
|
+
* it is the single most valuable string this product emits to an agent that has nothing connected.
|
|
24
|
+
* Replacing it with "timed out" would trade the answer for a stopwatch reading.
|
|
25
|
+
*/
|
|
26
|
+
/** How often to re-ask. A session appearing is an out-of-band event; this is a plain poll. */
|
|
27
|
+
const POLL_MS = 250;
|
|
28
|
+
export async function resolveSessionWithin(sessions, sessionId, timeoutMs, deps) {
|
|
29
|
+
try {
|
|
30
|
+
return sessions.resolve(sessionId);
|
|
31
|
+
}
|
|
32
|
+
catch (first) {
|
|
33
|
+
// Nothing to wait FOR unless the map is empty, and nothing to wait WITH unless a budget was
|
|
34
|
+
// given. Either way the caller gets the same error it would have got before.
|
|
35
|
+
if (timeoutMs <= 0 || sessions.count() > 0)
|
|
36
|
+
throw first;
|
|
37
|
+
const deadline = deps.now() + timeoutMs;
|
|
38
|
+
for (;;) {
|
|
39
|
+
const remaining = deadline - deps.now();
|
|
40
|
+
if (remaining <= 0)
|
|
41
|
+
throw first;
|
|
42
|
+
await deps.sleep(Math.min(POLL_MS, remaining));
|
|
43
|
+
try {
|
|
44
|
+
return sessions.resolve(sessionId);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Still nothing. Keep the FIRST error: it was raised when the daemon had most recently
|
|
48
|
+
// computed its diagnosis, and re-throwing the newest one would report the same fact with a
|
|
49
|
+
// staler explanation.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -8,6 +8,15 @@ export interface SessionInfo {
|
|
|
8
8
|
title?: string;
|
|
9
9
|
adapters: string[];
|
|
10
10
|
hasCapabilities: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Which shell answered: `web`, `electron` or `tauri`.
|
|
13
|
+
*
|
|
14
|
+
* Absent — never defaulted to `web` — on an SDK too old to report one, because the guess is wrong
|
|
15
|
+
* on exactly the machines this exists to tell apart. Two windows on the same url are otherwise
|
|
16
|
+
* indistinguishable, and a browser tab is not a desktop app: it has none of its IPC, and it does
|
|
17
|
+
* not render like it.
|
|
18
|
+
*/
|
|
19
|
+
runtime?: string;
|
|
11
20
|
/** Present only when the page's SDK version differs from the daemon's — see version-skew.ts. */
|
|
12
21
|
versionSkew?: string;
|
|
13
22
|
/** ms since the SDK last reported anything (silence ⇒ likely throttled). */
|
|
@@ -19,6 +28,13 @@ export interface SessionInfo {
|
|
|
19
28
|
recommendation?: string;
|
|
20
29
|
stale?: boolean;
|
|
21
30
|
cleanup_suggestion?: string;
|
|
31
|
+
/**
|
|
32
|
+
* present ONLY when this tab has stopped answering commands — attached, streaming events, and
|
|
33
|
+
* missing every command budget. Never `false`: absence means "answering, or not asked yet".
|
|
34
|
+
*/
|
|
35
|
+
unresponsive?: true;
|
|
36
|
+
/** present with `unresponsive` — the in-protocol way out of a wedged tab. */
|
|
37
|
+
unresponsive_suggestion?: string;
|
|
22
38
|
/** present only when the human has flagged bugs on this tab — count of pending review marks. */
|
|
23
39
|
pendingMarks?: number;
|
|
24
40
|
/** present with pendingMarks — nudges the agent to drain them with reticle_review. */
|
|
@@ -47,11 +63,13 @@ interface SessionView {
|
|
|
47
63
|
title: string;
|
|
48
64
|
adapters: string[];
|
|
49
65
|
hasCapabilities: boolean;
|
|
66
|
+
runtime: string | undefined;
|
|
50
67
|
versionSkew: string | undefined;
|
|
51
68
|
hidden: boolean;
|
|
52
69
|
health: () => SessionHealth;
|
|
53
70
|
staleMs: () => number;
|
|
54
71
|
pendingMarkCount: () => number;
|
|
72
|
+
unresponsive: () => boolean;
|
|
55
73
|
}
|
|
56
74
|
/**
|
|
57
75
|
* Project a live Session into the shape the tools return.
|
|
@@ -25,6 +25,8 @@ export function buildSessionInfo(session) {
|
|
|
25
25
|
...('' === session.title.trim() ? {} : { title: session.title }),
|
|
26
26
|
adapters: session.adapters,
|
|
27
27
|
hasCapabilities: session.hasCapabilities,
|
|
28
|
+
// Omitted when the page never said, so absence stays readable as "unknown" rather than "web".
|
|
29
|
+
...(session.runtime === undefined ? {} : { runtime: session.runtime }),
|
|
28
30
|
// On every listing, not buried in a log — skew explains failures that read as app bugs.
|
|
29
31
|
...(session.versionSkew === undefined ? {} : { versionSkew: session.versionSkew }),
|
|
30
32
|
hidden: session.hidden,
|
|
@@ -35,6 +37,15 @@ export function buildSessionInfo(session) {
|
|
|
35
37
|
base.cleanup_suggestion =
|
|
36
38
|
'Call reticle_session{action:"end"} to free this session before starting new work.';
|
|
37
39
|
}
|
|
40
|
+
// A tab that is attached and answering nothing. Said out loud because every health field beside it
|
|
41
|
+
// reads fine — that combination is exactly what made the wedge invisible.
|
|
42
|
+
if (session.unresponsive()) {
|
|
43
|
+
base.unresponsive = true;
|
|
44
|
+
base.unresponsive_suggestion =
|
|
45
|
+
'This tab is connected but has stopped answering commands, so every call against it will time ' +
|
|
46
|
+
'out. Ending the session does not revive the page. Reload the tab, or run `reticle open <url>` ' +
|
|
47
|
+
'to get a fresh one — it will no longer hand this tab back.';
|
|
48
|
+
}
|
|
38
49
|
// Surface human bug reports in reticle_sessions (only when > 0, so a clean session adds nothing).
|
|
39
50
|
const marks = session.pendingMarkCount();
|
|
40
51
|
if (marks > 0) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NoSessionReason } from '@reticlehq/core';
|
|
1
2
|
import { Session, type SessionInfo } from './session.js';
|
|
2
3
|
import { AttachmentHistory } from './attachment-history.js';
|
|
3
4
|
import type { NoSessionNextAction } from './no-session-next-action.js';
|
|
@@ -37,6 +38,8 @@ export declare class SessionManager {
|
|
|
37
38
|
* already computed for the error path; this exposes it to the read path too.
|
|
38
39
|
*/
|
|
39
40
|
noSessionHint(): string | undefined;
|
|
41
|
+
setNoSessionReason(reason: (() => NoSessionReason | undefined) | undefined): void;
|
|
42
|
+
noSessionReason(): NoSessionReason | undefined;
|
|
40
43
|
setNoSessionNextAction(next: (() => NoSessionNextAction | undefined) | undefined): void;
|
|
41
44
|
noSessionNextAction(): NoSessionNextAction | undefined;
|
|
42
45
|
everConnected(): boolean;
|
|
@@ -47,6 +50,17 @@ export declare class SessionManager {
|
|
|
47
50
|
at: number;
|
|
48
51
|
reason: string;
|
|
49
52
|
} | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* The id of the session that most recently went away, if any.
|
|
55
|
+
*
|
|
56
|
+
* Distinct from {@link lastClosure}, which only records closes the bridge itself initiated
|
|
57
|
+
* (handshake refusals, auth failures) — an ordinary disconnect never reaches it. This reads the
|
|
58
|
+
* tombstone ring `remove()` maintains, so it covers every departure.
|
|
59
|
+
*
|
|
60
|
+
* Read by the no-session diagnosis, which needs to say something about the session that actually
|
|
61
|
+
* vanished rather than about the daemon's lifetime history (#611).
|
|
62
|
+
*/
|
|
63
|
+
lastDeparted(): string | undefined;
|
|
50
64
|
resolve(sessionId?: string, scope?: ResolveScope): Session;
|
|
51
65
|
}
|
|
52
66
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NO_SESSION_CONNECTED_ERROR } from '@reticlehq/core';
|
|
2
|
+
import { notePendingNoSessionReason } from '../telemetry/tool-refused.js';
|
|
2
3
|
import { declareDrivenRedactionKeys, forgetDrivenRedactionKeys, } from '../input/driven-redaction.js';
|
|
3
4
|
import { Session } from './session.js';
|
|
4
5
|
import { AttachmentHistory } from './attachment-history.js';
|
|
@@ -111,6 +112,11 @@ export class SessionManager {
|
|
|
111
112
|
this.#recordConnection?.(session.projectId);
|
|
112
113
|
const previous = this.#sessions.get(session.id);
|
|
113
114
|
this.#sessions.set(session.id, session);
|
|
115
|
+
// Who can SEE this session's HUD feed. Wired here because `add` is the one method every path
|
|
116
|
+
// that registers a session goes through, and read lazily so a tab that opens later still counts.
|
|
117
|
+
// Optional-call for the same reason `pushImpact` is one at the dispatch chokepoint: a test
|
|
118
|
+
// double is a partial Session, and wiring a courtesy channel must never break registration.
|
|
119
|
+
session.setViewers?.(() => this.#viewersFor(session));
|
|
114
120
|
// Publish what this app declared sensitive to the driven-path rule. Here rather than in the
|
|
115
121
|
// bridge because EVERY path that registers a session goes through this method, and a declaration
|
|
116
122
|
// that silently fails to register is a leak nothing would report.
|
|
@@ -119,6 +125,17 @@ export class SessionManager {
|
|
|
119
125
|
this.#tombstones.delete(session.id);
|
|
120
126
|
return previous;
|
|
121
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* The other tabs of the same app as `driven`.
|
|
130
|
+
*
|
|
131
|
+
* Scoped the same way auto-selection is: by the stable projectId when the driven tab stamped one,
|
|
132
|
+
* else by origin. A drive session is frequently a headless pooled context (see Session.#viewers),
|
|
133
|
+
* so without this the human's own tab is the one place the report never reaches.
|
|
134
|
+
*/
|
|
135
|
+
#viewersFor(driven) {
|
|
136
|
+
const scope = driven.projectId === undefined ? { url: driven.url } : { projectId: driven.projectId };
|
|
137
|
+
return scopeSessions([...this.#sessions.values()], scope).filter((s) => s !== driven);
|
|
138
|
+
}
|
|
122
139
|
remove(session) {
|
|
123
140
|
if (this.#sessions.get(session.id) !== session)
|
|
124
141
|
return false;
|
|
@@ -218,6 +235,13 @@ export class SessionManager {
|
|
|
218
235
|
* bridge constructed without a daemon (every unit test) keeps the plain message.
|
|
219
236
|
*/
|
|
220
237
|
#noSessionHint;
|
|
238
|
+
/**
|
|
239
|
+
* The CODE for the same diagnosis `#noSessionHint` renders as prose.
|
|
240
|
+
*
|
|
241
|
+
* Registered from the same `explainNoSession` call as the hint, so the two cannot describe
|
|
242
|
+
* different branches. See no-session-watch.ts.
|
|
243
|
+
*/
|
|
244
|
+
#noSessionReason;
|
|
221
245
|
/** Wire the diagnosis provider (daemon boot). Absent ⇒ the plain, static message. */
|
|
222
246
|
setNoSessionHint(hint) {
|
|
223
247
|
this.#noSessionHint = hint;
|
|
@@ -233,6 +257,12 @@ export class SessionManager {
|
|
|
233
257
|
noSessionHint() {
|
|
234
258
|
return this.#noSessionHint?.();
|
|
235
259
|
}
|
|
260
|
+
setNoSessionReason(reason) {
|
|
261
|
+
this.#noSessionReason = reason;
|
|
262
|
+
}
|
|
263
|
+
noSessionReason() {
|
|
264
|
+
return this.#noSessionReason?.();
|
|
265
|
+
}
|
|
236
266
|
/**
|
|
237
267
|
* The same diagnosis with the prose taken out — one action kind and one literal command.
|
|
238
268
|
*
|
|
@@ -262,6 +292,22 @@ export class SessionManager {
|
|
|
262
292
|
lastClosure() {
|
|
263
293
|
return this.#recentClosures[this.#recentClosures.length - 1];
|
|
264
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* The id of the session that most recently went away, if any.
|
|
297
|
+
*
|
|
298
|
+
* Distinct from {@link lastClosure}, which only records closes the bridge itself initiated
|
|
299
|
+
* (handshake refusals, auth failures) — an ordinary disconnect never reaches it. This reads the
|
|
300
|
+
* tombstone ring `remove()` maintains, so it covers every departure.
|
|
301
|
+
*
|
|
302
|
+
* Read by the no-session diagnosis, which needs to say something about the session that actually
|
|
303
|
+
* vanished rather than about the daemon's lifetime history (#611).
|
|
304
|
+
*/
|
|
305
|
+
lastDeparted() {
|
|
306
|
+
let last;
|
|
307
|
+
for (const id of this.#tombstones.keys())
|
|
308
|
+
last = id;
|
|
309
|
+
return last;
|
|
310
|
+
}
|
|
265
311
|
/**
|
|
266
312
|
* A dead `sessionId`, answered with what the caller needs to recover — not with an errand.
|
|
267
313
|
*
|
|
@@ -307,6 +353,10 @@ export class SessionManager {
|
|
|
307
353
|
const refusal = closure === undefined
|
|
308
354
|
? ''
|
|
309
355
|
: ` NOTE: the bridge REFUSED or closed a connection recently — "${closure.reason}". The app is probably still running and trying to connect: it was turned away, and the SDK does not retry after a policy close. The reason above names the fix — do not go looking for a stopped dev server.`;
|
|
356
|
+
// Hand the branch code to the refusal that is about to be reported for this throw. The
|
|
357
|
+
// diagnosis is computed HERE and the refusal is classified from the message downstream, so
|
|
358
|
+
// without this the largest refusal cohort stays one undifferentiated bucket (#615).
|
|
359
|
+
notePendingNoSessionReason(this.#noSessionReason?.());
|
|
310
360
|
throw new Error(`${hint ?? NO_SESSION_CONNECTED_ERROR}${refusal}`);
|
|
311
361
|
}
|
|
312
362
|
// Scope to the agent's active project FIRST, so a stray tab from another app/origin (e.g. a
|
|
@@ -357,8 +407,15 @@ export class SessionManager {
|
|
|
357
407
|
'several apps leaves it unable to tell them apart.');
|
|
358
408
|
}
|
|
359
409
|
// Multiple sessions: score each (lower = better candidate for auto-selection).
|
|
360
|
-
// 0 = non-throttled (visible + recently-heard), 1 = throttled (hidden or stale heartbeat)
|
|
361
|
-
|
|
410
|
+
// 0 = non-throttled (visible + recently-heard), 1 = throttled (hidden or stale heartbeat),
|
|
411
|
+
// 2 = attached but not answering commands. A wedged tab keeps streaming events, so recency
|
|
412
|
+
// RATES IT HIGHEST — it was beating a healthy sibling on the one signal scoring looked at, and
|
|
413
|
+
// every call auto-targeted the one tab guaranteed to time out.
|
|
414
|
+
const scored = all.map((s) => ({
|
|
415
|
+
s,
|
|
416
|
+
score: s.unresponsive() ? 2 : s.throttled() ? 1 : 0,
|
|
417
|
+
ms: s.lastSeenMs(),
|
|
418
|
+
}));
|
|
362
419
|
const bestScore = Math.min(...scored.map((x) => x.score));
|
|
363
420
|
const candidates = scored.filter((x) => x.score === bestScore);
|
|
364
421
|
// Sort candidates by recency (ascending lastSeenMs = most recently active first).
|
|
@@ -375,7 +432,8 @@ export class SessionManager {
|
|
|
375
432
|
// desktop), the gap requirement is dropped: every session is already in "background" mode
|
|
376
433
|
// so we just pick the one with the freshest heartbeat and let the agent proceed. Requiring
|
|
377
434
|
// a gap here only produces spurious "ambiguous" errors while the user works elsewhere.
|
|
378
|
-
|
|
435
|
+
// Nothing healthy left to choose between (all throttled, or all wedged): drop the gap check.
|
|
436
|
+
const allThrottled = bestScore >= 1;
|
|
379
437
|
const RECENCY_GAP_MS = allThrottled ? 0 : 1_000;
|
|
380
438
|
const clearWinner = runnerUp === undefined || best.ms + RECENCY_GAP_MS < runnerUp.ms;
|
|
381
439
|
if (!clearWinner) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a tab that is only WATCHING gets to see of a drive happening somewhere else.
|
|
3
|
+
*
|
|
4
|
+
* Its own module because it is its own question — the rest of session.ts is about a session Reticle
|
|
5
|
+
* is DRIVING, and this is about the one it is not. Split out when session.ts crossed the 1000-line
|
|
6
|
+
* cap; the cap did its job, because these two had no reason to share a file beyond history.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* How a mirrored row is introduced on a tab that is only WATCHING.
|
|
10
|
+
*
|
|
11
|
+
* The row happened somewhere else, and a HUD that prints it unlabelled tells the reader their own
|
|
12
|
+
* tab was driven. Naming the session is also the handle they need to address it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function mirroredNarration(sessionId: string, text: string): string;
|
|
15
|
+
/** The pushes that are a REPORT of what happened, and so are worth showing to a watching tab. */
|
|
16
|
+
export declare const MIRRORED_COMMANDS: ReadonlySet<string>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a tab that is only WATCHING gets to see of a drive happening somewhere else.
|
|
3
|
+
*
|
|
4
|
+
* Its own module because it is its own question — the rest of session.ts is about a session Reticle
|
|
5
|
+
* is DRIVING, and this is about the one it is not. Split out when session.ts crossed the 1000-line
|
|
6
|
+
* cap; the cap did its job, because these two had no reason to share a file beyond history.
|
|
7
|
+
*/
|
|
8
|
+
import { ReticleCommand } from '@reticlehq/core';
|
|
9
|
+
/**
|
|
10
|
+
* How a mirrored row is introduced on a tab that is only WATCHING.
|
|
11
|
+
*
|
|
12
|
+
* The row happened somewhere else, and a HUD that prints it unlabelled tells the reader their own
|
|
13
|
+
* tab was driven. Naming the session is also the handle they need to address it.
|
|
14
|
+
*/
|
|
15
|
+
export function mirroredNarration(sessionId, text) {
|
|
16
|
+
return `${sessionId} \u2192 ${text}`;
|
|
17
|
+
}
|
|
18
|
+
/** The pushes that are a REPORT of what happened, and so are worth showing to a watching tab. */
|
|
19
|
+
export const MIRRORED_COMMANDS = new Set([
|
|
20
|
+
ReticleCommand.NARRATE,
|
|
21
|
+
ReticleCommand.IMPACT,
|
|
22
|
+
]);
|
|
@@ -6,12 +6,26 @@ export interface RecommendationInputs {
|
|
|
6
6
|
hidden: boolean;
|
|
7
7
|
throttled: boolean;
|
|
8
8
|
focused: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The shell this session runs in, as the SDK reported it at handshake. Undefined for an older SDK
|
|
11
|
+
* that does not report one — and an unknown runtime keeps the WEB advice, because a browser tab is
|
|
12
|
+
* what most sessions are and withholding a usable escape hatch is the worse error.
|
|
13
|
+
*/
|
|
14
|
+
runtime?: string | undefined;
|
|
9
15
|
}
|
|
10
16
|
/**
|
|
11
|
-
* A human-readable
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* A human-readable hint when a tab is hidden or throttled. Returns undefined for a healthy tab so
|
|
18
|
+
* the field stays ABSENT (not empty). A merely-unfocused but live tab is still scriptable, so blur
|
|
19
|
+
* alone does not trigger it. Pure.
|
|
20
|
+
*
|
|
21
|
+
* HIDDEN and THROTTLED get DIFFERENT answers, which is the fix. They used to share one message that
|
|
22
|
+
* said "may be un-focusable; acquire a guaranteed scriptable context" — correct for a background
|
|
23
|
+
* tab, and actively harmful for a visible one. A visible throttled tab is driveable; measured in the
|
|
24
|
+
* field, one took a sign-in and two clean net-grade verdicts with no retries, while that message had
|
|
25
|
+
* already sent the agent into a lease the watching human could not see. One flag, advice that cost
|
|
26
|
+
* the product's main trust surface, and nothing bought.
|
|
27
|
+
*
|
|
28
|
+
* Hidden outranks throttled when both are set: a background tab is the stronger fact, and its
|
|
29
|
+
* failure mode (events landing on a page that never advances) is the one worth warning about.
|
|
16
30
|
*/
|
|
17
31
|
export declare function buildSessionRecommendation(inputs: RecommendationInputs): string | undefined;
|
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AppRuntime, DESKTOP_WINDOW_BACKGROUNDED, HIDDEN_TAB_RECOMMENDATION, THROTTLED_TAB_RECOMMENDATION, } from '@reticlehq/core';
|
|
2
2
|
/**
|
|
3
|
-
* A human-readable
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* A human-readable hint when a tab is hidden or throttled. Returns undefined for a healthy tab so
|
|
4
|
+
* the field stays ABSENT (not empty). A merely-unfocused but live tab is still scriptable, so blur
|
|
5
|
+
* alone does not trigger it. Pure.
|
|
6
|
+
*
|
|
7
|
+
* HIDDEN and THROTTLED get DIFFERENT answers, which is the fix. They used to share one message that
|
|
8
|
+
* said "may be un-focusable; acquire a guaranteed scriptable context" — correct for a background
|
|
9
|
+
* tab, and actively harmful for a visible one. A visible throttled tab is driveable; measured in the
|
|
10
|
+
* field, one took a sign-in and two clean net-grade verdicts with no retries, while that message had
|
|
11
|
+
* already sent the agent into a lease the watching human could not see. One flag, advice that cost
|
|
12
|
+
* the product's main trust surface, and nothing bought.
|
|
13
|
+
*
|
|
14
|
+
* Hidden outranks throttled when both are set: a background tab is the stronger fact, and its
|
|
15
|
+
* failure mode (events landing on a page that never advances) is the one worth warning about.
|
|
8
16
|
*/
|
|
9
17
|
export function buildSessionRecommendation(inputs) {
|
|
10
|
-
if (inputs.hidden
|
|
11
|
-
return
|
|
12
|
-
|
|
18
|
+
if (!inputs.hidden && !inputs.throttled)
|
|
19
|
+
return undefined;
|
|
20
|
+
// A desktop window gets the one answer it can act on. Checked BEFORE hidden/throttled are told
|
|
21
|
+
// apart, because the distinction between them is about browser tab lifecycle and neither of its
|
|
22
|
+
// two answers exists for an app whose window is the client. See DESKTOP_WINDOW_BACKGROUNDED.
|
|
23
|
+
if (AppRuntime.ELECTRON === inputs.runtime || AppRuntime.TAURI === inputs.runtime) {
|
|
24
|
+
return DESKTOP_WINDOW_BACKGROUNDED;
|
|
25
|
+
}
|
|
26
|
+
if (inputs.hidden)
|
|
27
|
+
return HIDDEN_TAB_RECOMMENDATION;
|
|
28
|
+
return THROTTLED_TAB_RECOMMENDATION;
|
|
13
29
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Unique at the departed origin (and project, when known). Two live tabs at that origin is still a
|
|
10
10
|
* guess, and a guess here would drive the wrong app.
|
|
11
11
|
*/
|
|
12
|
+
import { RETICLE_URL_PARAM } from '@reticlehq/core';
|
|
12
13
|
const POLL_MS = 25;
|
|
13
14
|
const REAL_CLOCK = {
|
|
14
15
|
now: () => Date.now(),
|
|
@@ -22,6 +23,39 @@ function originOf(url) {
|
|
|
22
23
|
return undefined;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The session a URL explicitly CLAIMS to be, via `__reticle_session`.
|
|
28
|
+
*
|
|
29
|
+
* A leased tab carries this marker so the pool can find the context it opened, and an app can set it
|
|
30
|
+
* deliberately so a battery addresses a known id. Either way it is a claim of identity, not a
|
|
31
|
+
* coincidence of origin — which is what makes it the right thing to gate succession on.
|
|
32
|
+
*/
|
|
33
|
+
function claimedSessionOf(url) {
|
|
34
|
+
try {
|
|
35
|
+
return new URL(url).searchParams.get(RETICLE_URL_PARAM.SESSION) ?? undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether one session may inherit another's identity.
|
|
43
|
+
*
|
|
44
|
+
* Origin alone is not enough. A leased tab is an isolated context that an AGENT owns and no human
|
|
45
|
+
* can see; the developer's own tab at the same origin is a different trust and visibility domain
|
|
46
|
+
* entirely. Letting one succeed the other meant an expired lease silently redirected the next call
|
|
47
|
+
* into somebody's real browser tab — observed, twice, and reported as success.
|
|
48
|
+
*
|
|
49
|
+
* So a session that claims an explicit identity only ever succeeds to one claiming the SAME
|
|
50
|
+
* identity. A session that claims none is unaffected, which is the ordinary reload case this whole
|
|
51
|
+
* mechanism exists for.
|
|
52
|
+
*/
|
|
53
|
+
function mayInherit(departed, candidate) {
|
|
54
|
+
const claimed = claimedSessionOf(departed.url);
|
|
55
|
+
if (claimed === undefined)
|
|
56
|
+
return true;
|
|
57
|
+
return claimedSessionOf(candidate.url) === claimed;
|
|
58
|
+
}
|
|
25
59
|
function identityOf(session) {
|
|
26
60
|
return {
|
|
27
61
|
id: session.id,
|
|
@@ -44,7 +78,8 @@ export function pickDocumentSuccessor(live, departed) {
|
|
|
44
78
|
if (sameId !== undefined)
|
|
45
79
|
return sameId;
|
|
46
80
|
const others = atOrigin.filter((s) => s.id !== departed.id &&
|
|
47
|
-
(departed.projectId === undefined || s.projectId === departed.projectId)
|
|
81
|
+
(departed.projectId === undefined || s.projectId === departed.projectId) &&
|
|
82
|
+
mayInherit(departed, s));
|
|
48
83
|
if (1 !== others.length)
|
|
49
84
|
return undefined;
|
|
50
85
|
return others[0];
|
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { ReticleCommand } from '@reticlehq/core';
|
|
3
3
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
4
4
|
import { asNumber, asString } from '../tools/tools-helpers.js';
|
|
5
|
+
import { idleMsSchema } from '../tools/numeric-bounds.js';
|
|
5
6
|
/**
|
|
6
7
|
* Session lifecycle controls. The presenter session begins on the agent's first action and ends
|
|
7
8
|
* either when the agent ends it (reticle_end_session) or after an idle window — and the human keeps the
|
|
@@ -13,8 +14,7 @@ export const SESSION_TOOLS = [
|
|
|
13
14
|
name: ReticleTool.SESSION_TUNE,
|
|
14
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
16
|
inputSchema: {
|
|
16
|
-
idleEndMs:
|
|
17
|
-
.number()
|
|
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
20
|
sessionId: z
|
|
@@ -26,6 +26,17 @@ export declare class Session {
|
|
|
26
26
|
readonly id: string;
|
|
27
27
|
/** Stable build-stamped project identity; undefined for v1.0 SDKs that omit it. */
|
|
28
28
|
readonly projectId: string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The `.reticle` directory this session's evidence belongs in — impact counters AND capsules.
|
|
31
|
+
*
|
|
32
|
+
* Stamped once when the session is created, from the project it declared in HELLO, rather than
|
|
33
|
+
* resolved per call: the answer cannot change while the tab is connected, and resolving it once
|
|
34
|
+
* means every counter that fires for this session agrees about where it goes.
|
|
35
|
+
*
|
|
36
|
+
* Undefined when nothing could name a project — the caller then falls back to the daemon's own
|
|
37
|
+
* root, which is what every counter did unconditionally before this existed.
|
|
38
|
+
*/
|
|
39
|
+
artifactRoot: string | undefined;
|
|
29
40
|
url: string;
|
|
30
41
|
title: string;
|
|
31
42
|
adapters: string[];
|
|
@@ -62,6 +73,13 @@ export declare class Session {
|
|
|
62
73
|
/** The attachable health block — single source of truth for the tools. */
|
|
63
74
|
health(): SessionHealth;
|
|
64
75
|
info(): SessionInfo;
|
|
76
|
+
/**
|
|
77
|
+
* Attached but not answering. A tab can be `connected` with a fresh `lastSeenMs` and still answer
|
|
78
|
+
* nothing: `lastSeenMs` measures the events the SDK streams, not the commands it replies to. Reuse
|
|
79
|
+
* and auto-selection read that as health, so `reticle open` handed a wedged tab back forever and
|
|
80
|
+
* `reticle_session{action:"end"}` — a flag on the record, not on the page — could not help.
|
|
81
|
+
*/
|
|
82
|
+
unresponsive(): boolean;
|
|
65
83
|
/** Wall-clock age of the session in milliseconds. */
|
|
66
84
|
staleMs(): number;
|
|
67
85
|
/** The document the most recent stamped event was observed under. See `#documentId`. */
|
|
@@ -187,10 +205,10 @@ export declare class Session {
|
|
|
187
205
|
command(name: string, args?: Record<string, unknown>, timeoutMs?: number): Promise<CommandResult>;
|
|
188
206
|
handleResult(result: CommandResult): void;
|
|
189
207
|
/** Reject everything still in flight — used on disconnect. */
|
|
190
|
-
rejectAll(reason: string): void;
|
|
208
|
+
rejectAll(reason: string, replaced?: boolean): void;
|
|
191
209
|
succeededBy(next: Session): void;
|
|
192
210
|
/** End this transport without letting a stale socket remove its replacement session. */
|
|
193
|
-
disconnect(reason: string): void;
|
|
211
|
+
disconnect(reason: string, replaced?: boolean): void;
|
|
194
212
|
getState(): SessionState;
|
|
195
213
|
isPaused(): boolean;
|
|
196
214
|
isEnded(): boolean;
|
|
@@ -243,6 +261,8 @@ export declare class Session {
|
|
|
243
261
|
* without needing an explicit polling loop.
|
|
244
262
|
*/
|
|
245
263
|
ageWarning(): string | undefined;
|
|
264
|
+
/** Wired by SessionManager.add, the one place every session registers. */
|
|
265
|
+
setViewers(read: () => Session[]): void;
|
|
246
266
|
}
|
|
247
267
|
/**
|
|
248
268
|
* Re-exported from session-manager.ts so the public import path (`./session.js`) is unchanged for
|