@reticlehq/server 3.1.0 → 3.2.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 +17 -18
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +13 -1
- package/dist/command/cli/cli-flow-commands.d.ts +2 -1
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse.d.ts +2 -1
- package/dist/command/cli/cli-parse.js +5 -2
- package/dist/command/cli/cli-verify.js +2 -2
- package/dist/command/cli/cloud-cli.js +7 -6
- package/dist/command/cli/cloud-kit.js +2 -6
- package/dist/command/cli/config/config-discovery.d.ts +12 -0
- package/dist/command/cli/config/config-discovery.js +19 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
- package/dist/command/cli/doctor/doctor-rows.js +12 -0
- package/dist/command/cli/drive/drive-command.js +1 -1
- package/dist/command/cli/launch/cli-launch.d.ts +10 -2
- package/dist/command/cli/launch/cli-launch.js +32 -39
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
- package/dist/command/cli/mcp-command.js +5 -3
- package/dist/command/cli/ports/resolve/cli-port.d.ts +2 -1
- package/dist/command/cli/ports/resolve/cli-port.js +5 -1
- package/dist/command/cli/status/status-lines.d.ts +28 -0
- package/dist/command/cli/status/status-lines.js +117 -0
- package/dist/command/cli.d.ts +1 -1
- package/dist/command/cli.js +24 -177
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +48 -0
- package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
- package/dist/command/daemon/binding/loopback-reach.js +63 -0
- package/dist/command/daemon/daemon-resilience.d.ts +13 -1
- package/dist/command/daemon/daemon-resilience.js +12 -1
- package/dist/command/daemon/daemon.d.ts +2 -11
- package/dist/command/daemon/daemon.js +8 -11
- package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
- package/dist/command/daemon/lifetime/heartbeat.js +10 -0
- package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
- package/dist/command/daemon/lifetime/log-cap.js +115 -0
- package/dist/command/setup/agent-notice-memory.d.ts +16 -0
- package/dist/command/setup/agent-notice-memory.js +65 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
- package/dist/command/setup/bringup/dev-server-wait.js +16 -0
- package/dist/command/setup/bringup/ensure-daemon.js +1 -1
- package/dist/command/setup/init/init-runtime.js +13 -1
- package/dist/command/setup/node-effects.js +1 -1
- package/dist/command/setup/probe/page-probe.js +9 -2
- package/dist/command/setup/run-setup.d.ts +25 -7
- package/dist/command/setup/run-setup.js +51 -9
- package/dist/command/setup/setup-command.js +31 -3
- package/dist/command/setup/terminal/confirm.js +2 -1
- package/dist/command/update/updater.d.ts +27 -0
- package/dist/command/update/updater.js +35 -2
- package/dist/features/crawl/crawl-tools.js +3 -1
- package/dist/features/crawl/crawl.js +2 -1
- package/dist/features/harness/drive-report.d.ts +73 -0
- package/dist/features/harness/drive-report.js +196 -0
- package/dist/features/harness/driver.js +29 -8
- package/dist/features/harness/drivers.d.ts +19 -0
- package/dist/features/harness/drivers.js +19 -0
- package/dist/features/harness/fill-values.d.ts +67 -0
- package/dist/features/harness/fill-values.js +153 -0
- package/dist/features/harness/jev-driver.d.ts +188 -0
- package/dist/features/harness/jev-driver.js +664 -0
- package/dist/features/harness/openai-driver.d.ts +46 -0
- package/dist/features/harness/openai-driver.js +197 -0
- package/dist/features/harness/platform-config.d.ts +52 -0
- package/dist/features/harness/platform-config.js +89 -0
- package/dist/features/visual/visual-tools.js +26 -2
- package/dist/index.js +60 -55
- package/dist/judgement/domain/domain-tools.js +4 -2
- package/dist/judgement/runs/verification-sync.d.ts +2 -2
- package/dist/judgement/runs/verification-sync.js +9 -2
- package/dist/language/flows/affected-tools.js +6 -3
- package/dist/language/flows/flow-replay-run.d.ts +25 -2
- package/dist/language/flows/flow-replay-run.js +73 -27
- package/dist/language/flows/flow-replay.d.ts +17 -0
- package/dist/language/flows/flow-replay.js +148 -100
- package/dist/language/flows/flow-result.d.ts +18 -1
- package/dist/language/flows/flow-result.js +18 -2
- package/dist/language/flows/flow-store-for-session.d.ts +2 -1
- package/dist/language/flows/flow-tools.js +8 -4
- package/dist/language/flows/flows.d.ts +8 -7
- package/dist/language/flows/flows.js +7 -3
- package/dist/language/flows/narrow-write.d.ts +4 -4
- package/dist/language/flows/replay.js +1 -1
- package/dist/language/flows/suite-selection.d.ts +15 -1
- package/dist/language/flows/suite-selection.js +1 -1
- package/dist/language/flows/verify-change-tools.js +7 -4
- package/dist/memory/cloud/account-state.d.ts +1 -1
- package/dist/memory/cloud/account-state.js +5 -4
- package/dist/memory/cloud/cloud-config.d.ts +9 -0
- package/dist/memory/cloud/cloud-config.js +9 -0
- package/dist/memory/cloud/cloud-sync.d.ts +7 -2
- package/dist/memory/cloud/cloud-sync.js +10 -5
- package/dist/memory/cloud/harness-config.d.ts +10 -0
- package/dist/memory/cloud/harness-config.js +58 -0
- package/dist/memory/cloud/harness-offer.d.ts +44 -0
- package/dist/memory/cloud/harness-offer.js +146 -0
- package/dist/memory/cloud/harness-switch.d.ts +11 -0
- package/dist/memory/cloud/harness-switch.js +53 -0
- package/dist/memory/cloud/sync-daemon.d.ts +22 -2
- package/dist/memory/cloud/sync-daemon.js +24 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +2 -1
- package/dist/memory/impact/impact-store.d.ts +5 -0
- package/dist/memory/impact/impact-store.js +19 -0
- package/dist/memory/intent/intent-shard-store.js +2 -2
- package/dist/memory/intent/intent-shard.d.ts +12 -12
- package/dist/memory/journal/attach-journal.d.ts +11 -0
- package/dist/memory/journal/attach-journal.js +1 -1
- package/dist/memory/journal/journal-query.d.ts +35 -1
- package/dist/memory/journal/journal-query.js +46 -0
- package/dist/memory/journal/journal-recorder.d.ts +50 -2
- package/dist/memory/journal/journal-recorder.js +45 -7
- package/dist/memory/journal/on-disk/retention.d.ts +87 -2
- package/dist/memory/journal/on-disk/retention.js +177 -8
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +9 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.js +9 -2
- package/dist/memory/journal/self-observation.d.ts +3 -0
- package/dist/memory/journal/self-observation.js +67 -0
- package/dist/memory/journal/session-end.d.ts +13 -3
- package/dist/memory/journal/session-end.js +24 -4
- package/dist/memory/journal/session-journal.d.ts +115 -5
- package/dist/memory/journal/session-journal.js +479 -32
- package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
- package/dist/memory/project/artifact-root-resolver.js +89 -0
- package/dist/memory/project/artifact-root.d.ts +39 -2
- package/dist/memory/project/artifact-root.js +58 -1
- package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
- package/dist/memory/project/dir/fill-value-store.js +88 -0
- package/dist/memory/project/dir/reticle-dir.d.ts +14 -4
- package/dist/memory/project/dir/reticle-dir.js +11 -0
- package/dist/memory/project/fs/fs-port.d.ts +22 -1
- package/dist/memory/project/fs/fs-port.js +33 -5
- package/dist/memory/project/learned-routes.d.ts +16 -2
- package/dist/memory/project/learned-routes.js +26 -10
- package/dist/memory/project/project-for-root.d.ts +30 -0
- package/dist/memory/project/project-for-root.js +43 -0
- package/dist/memory/project/project-tools.js +10 -5
- package/dist/memory/project/session-root.d.ts +35 -6
- package/dist/memory/project/session-root.js +70 -8
- package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
- package/dist/portal/bridge/auth-failure-reason.js +15 -1
- package/dist/portal/bridge/bridge.d.ts +6 -1
- package/dist/portal/bridge/bridge.js +36 -2
- package/dist/portal/session/facts/session-replaced.d.ts +19 -0
- package/dist/portal/session/facts/session-replaced.js +22 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -60
- package/dist/portal/session/no-session-next-action.js +3 -2
- package/dist/portal/session/no-session-watch.js +14 -10
- package/dist/portal/session/rebind.d.ts +18 -0
- package/dist/portal/session/rebind.js +38 -0
- package/dist/portal/session/session-manager.d.ts +4 -0
- package/dist/portal/session/session-manager.js +19 -1
- package/dist/portal/session/session.d.ts +21 -10
- package/dist/portal/session/session.js +29 -42
- package/dist/status-payload.d.ts +12 -1
- package/dist/status-payload.js +4 -1
- package/dist/surface/http-server.js +3 -5
- package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
- package/dist/surface/mcp/faults/mcp-outage.js +20 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
- package/dist/surface/mcp/mcp-proxy.js +20 -9
- package/dist/surface/mcp/mcp.d.ts +10 -1
- package/dist/surface/mcp/mcp.js +59 -29
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
- package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
- package/dist/surface/mcp/proxy/proxy-log.js +7 -22
- package/dist/surface/tools/act/act-preflight.js +6 -1
- package/dist/surface/tools/act/sequence-grade.js +13 -0
- package/dist/surface/tools/act-capsule.js +27 -1
- package/dist/surface/tools/assert/assert-verdict.js +44 -17
- package/dist/surface/tools/coverage-tools.js +5 -2
- package/dist/surface/tools/dynamic-tools.js +37 -46
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +15 -0
- package/dist/surface/tools/explore-tools.js +80 -7
- package/dist/surface/tools/feedback-tools.js +49 -2
- package/dist/surface/tools/first-sentence.d.ts +24 -0
- package/dist/surface/tools/first-sentence.js +0 -0
- package/dist/surface/tools/harness-explore.d.ts +125 -1
- package/dist/surface/tools/harness-explore.js +401 -15
- package/dist/surface/tools/harness-plan.d.ts +76 -0
- package/dist/surface/tools/harness-plan.js +156 -0
- package/dist/surface/tools/harness-toolset.js +12 -1
- package/dist/surface/tools/invoke-tool.js +65 -5
- package/dist/surface/tools/live-call-text.d.ts +6 -0
- package/dist/surface/tools/live-call-text.js +36 -2
- package/dist/surface/tools/observe-tools.js +55 -9
- package/dist/surface/tools/read/query-windowed.d.ts +1 -1
- package/dist/surface/tools/read/query-windowed.js +1 -1
- package/dist/surface/tools/read-tools.js +1 -2
- package/dist/surface/tools/tool-kit.d.ts +18 -3
- package/dist/surface/tools/tools-helpers.d.ts +8 -7
- package/dist/surface/tools/tools-helpers.js +11 -13
- package/dist/surface/tools/tools.js +6 -1
- package/dist/telemetry/bug-found.js +32 -2
- package/dist/telemetry/feedback-context.js +3 -3
- package/dist/telemetry/harness-drive.d.ts +6 -0
- package/dist/telemetry/harness-drive.js +39 -0
- package/dist/telemetry/mcp-connection.d.ts +14 -1
- package/dist/telemetry/mcp-connection.js +17 -2
- package/dist/telemetry/onboarding-firsts.d.ts +30 -1
- package/dist/telemetry/onboarding-firsts.js +54 -12
- package/dist/telemetry/verification-of.js +31 -3
- package/docs/agent-cheatsheet.md +7 -3
- package/docs/autodrive.md +42 -2
- package/docs/benchmarks.md +1 -1
- package/docs/capabilities.mdx +1 -1
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/status.mdx +35 -20
- package/docs/cli.mdx +8 -18
- package/docs/desktop.mdx +1 -1
- package/docs/faq.mdx +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +16 -0
- package/docs/system-map.md +6 -4
- package/docs/telemetry.md +2 -1
- package/docs/token-efficiency.md +2 -2
- package/docs/tools/overview.mdx +1 -1
- package/docs/troubleshooting.mdx +8 -2
- package/docs/usage.md +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project memory store for ONE artifact root.
|
|
3
|
+
*
|
|
4
|
+
* `deps.project` is bound to the daemon's own root at construction, which is right only when the
|
|
5
|
+
* daemon happens to have been started inside the app it is driving — and a user-scoped MCP
|
|
6
|
+
* registration, the common case, starts it wherever the editor's cwd is. So every run record, every
|
|
7
|
+
* learned route and every observability high-water mark was written into that directory: a
|
|
8
|
+
* `.reticle/` appearing in a backend nobody instrumented, and a driven project whose own memory
|
|
9
|
+
* stayed empty. The cloud half of `recordReplayRun` had already been moved onto the session's root
|
|
10
|
+
* and its local half left behind, which is how a replay outcome could reach the dashboard and not
|
|
11
|
+
* the project it belonged to.
|
|
12
|
+
*
|
|
13
|
+
* Reads and writes both go through here, deliberately: a write that resolves to the app while a
|
|
14
|
+
* read still answers from the daemon's directory is worse than either alone — the record lands and
|
|
15
|
+
* is then invisible to the tool that exists to report it.
|
|
16
|
+
*
|
|
17
|
+
* Rebinding is cheap (a filesystem port, a root and a clock) and the single-project case allocates
|
|
18
|
+
* nothing: when the resolved root IS the daemon's, `deps.project` comes back unchanged.
|
|
19
|
+
*/
|
|
20
|
+
import { ProjectStore } from './project-store.js';
|
|
21
|
+
export function projectForRoot(deps, root) {
|
|
22
|
+
if (root === deps.reticleRoot)
|
|
23
|
+
return deps.project;
|
|
24
|
+
return new ProjectStore(deps.fs, root, { now: deps.now });
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The same question asked by the daemon rather than by a tool: one store per artifact root, so a
|
|
28
|
+
* session's learned routes land in the app that was driven and not in whatever directory the daemon
|
|
29
|
+
* was launched from. The daemon's own store is seeded in, so the single-project case allocates
|
|
30
|
+
* nothing and keeps the instance every other caller already holds.
|
|
31
|
+
*/
|
|
32
|
+
export function projectStoreResolver(fs, daemonStore, daemonRoot, now) {
|
|
33
|
+
const byRoot = new Map([[daemonRoot, daemonStore]]);
|
|
34
|
+
return (root) => {
|
|
35
|
+
const key = root ?? daemonRoot;
|
|
36
|
+
let found = byRoot.get(key);
|
|
37
|
+
if (found === undefined) {
|
|
38
|
+
found = new ProjectStore(fs, key, { now });
|
|
39
|
+
byRoot.set(key, found);
|
|
40
|
+
}
|
|
41
|
+
return found;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { sessionRoot } from './session-root.js';
|
|
3
|
+
import { projectForRoot } from './project-for-root.js';
|
|
2
4
|
import { ProjectReadError, RunStatus } from '@reticlehq/core';
|
|
3
5
|
import { ReticleTool } from '@reticlehq/core';
|
|
4
6
|
import { countSchema } from '../../surface/tools/args/numeric-bounds.js';
|
|
@@ -67,9 +69,11 @@ function lastTwoFor(runs, name) {
|
|
|
67
69
|
* The per-flow diff between the two most-recent verification ARTIFACTS (.reticle/runs), or undefined when
|
|
68
70
|
* fewer than two exist. Never throws — a missing/unreadable artifact must not break reading run history.
|
|
69
71
|
*/
|
|
70
|
-
async function lastTwoRunArtifacts(deps
|
|
72
|
+
async function lastTwoRunArtifacts(deps,
|
|
73
|
+
/** The session's root — the same address `persistAndSyncVerificationRun` wrote the pair to. */
|
|
74
|
+
root) {
|
|
71
75
|
try {
|
|
72
|
-
const pair = await new RunStore(deps.fs,
|
|
76
|
+
const pair = await new RunStore(deps.fs, root).latestTwo();
|
|
73
77
|
return pair === undefined ? undefined : diffVerificationRuns(pair[0], pair[1]);
|
|
74
78
|
}
|
|
75
79
|
catch {
|
|
@@ -106,7 +110,8 @@ export const PROJECT_TOOLS = [
|
|
|
106
110
|
handler: async (deps, args) => {
|
|
107
111
|
const cloud = await cloudRegression(deps, asString(args['sessionId']));
|
|
108
112
|
const withCloud = (obj) => cloud === undefined ? obj : { ...obj, cloud };
|
|
109
|
-
const
|
|
113
|
+
const project = projectForRoot(deps, sessionRoot(deps, asString(args['sessionId'])));
|
|
114
|
+
const read = await project.read();
|
|
110
115
|
if (!read.ok) {
|
|
111
116
|
return withCloud({
|
|
112
117
|
error: read.reason === ProjectReadError.MISSING
|
|
@@ -133,14 +138,14 @@ export const PROJECT_TOOLS = [
|
|
|
133
138
|
learned: read.file.learned,
|
|
134
139
|
});
|
|
135
140
|
}
|
|
136
|
-
const lastRun = await
|
|
141
|
+
const lastRun = await project.lastRun(name);
|
|
137
142
|
const pair = lastTwoFor(read.file.runs, name);
|
|
138
143
|
//: the RICH run diff (per-flow duration deltas past a noise floor, status changes, new/removed
|
|
139
144
|
// flows, verdict change) over the last two verification ARTIFACTS. That lives alongside — not
|
|
140
145
|
// instead of — the lightweight RunRecord diff above: `diff` answers "did this named run behave like
|
|
141
146
|
// last time?" from project.json, while `runDiff` answers "what changed between the last two full
|
|
142
147
|
// verification runs?" from .reticle/runs. Best-effort: no artifacts simply means no runDiff.
|
|
143
|
-
const runDiff = await lastTwoRunArtifacts(deps);
|
|
148
|
+
const runDiff = await lastTwoRunArtifacts(deps, sessionRoot(deps, asString(args['sessionId'])));
|
|
144
149
|
const forName = read.file.runs.filter((r) => r.name === name);
|
|
145
150
|
return withCloud({
|
|
146
151
|
runs: recent(forName),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProjectId } from '@reticlehq/core';
|
|
1
2
|
import type { ToolDeps } from '../../surface/tools/tool-kit.js';
|
|
2
3
|
/**
|
|
3
4
|
* The `.reticle` directory a tool call's artifacts belong in.
|
|
@@ -9,15 +10,43 @@ import type { ToolDeps } from '../../surface/tools/tool-kit.js';
|
|
|
9
10
|
* fixed — the flow would save successfully and then be invisible to the tool that exists to replay
|
|
10
11
|
* it, which reads as "no flows covered this change" rather than as an error.
|
|
11
12
|
*
|
|
12
|
-
* Falls back to `deps.reticleRoot` whenever resolution cannot name a project
|
|
13
|
-
*
|
|
13
|
+
* Falls back to `deps.reticleRoot` whenever resolution cannot name a project — EXCEPT when the
|
|
14
|
+
* caller named a session that resolves to nothing, which is the one case where the fallback is a
|
|
15
|
+
* wrong answer rather than a cautious one. See `sessionProjectId`.
|
|
14
16
|
*/
|
|
15
17
|
export declare function sessionRoot(deps: ToolDeps, sessionId: string | undefined): string;
|
|
18
|
+
/**
|
|
19
|
+
* The same answer for a caller that already holds the projectId rather than a sessionId.
|
|
20
|
+
*
|
|
21
|
+
* The suite paths resolve the project ONCE and thread it down -- the run artifact, the cloud link,
|
|
22
|
+
* the flake ledger and the flows all belong to that one project -- so asking them to go back to the
|
|
23
|
+
* session manager for an id they already have is how a caller ends up taking the root from one
|
|
24
|
+
* place and the id from another. `sessionRoot` is now this function plus a lookup, so the two
|
|
25
|
+
* cannot disagree about what a resolved root is.
|
|
26
|
+
*/
|
|
27
|
+
export declare function rootForProjectId(deps: ToolDeps, projectId: ProjectId | undefined): string;
|
|
28
|
+
/**
|
|
29
|
+
* The PROJECT directory -- one level above `.reticle` -- for a caller that runs a tool in a tree
|
|
30
|
+
* rather than writing a file into one. `git diff` is the case: it was being run in the daemon's
|
|
31
|
+
* `.reticle` subdirectory, which answers about the wrong repository (or, from a globally-registered
|
|
32
|
+
* daemon at `/` or `$HOME`, about none) while reporting a clean "nothing changed".
|
|
33
|
+
*/
|
|
34
|
+
export declare function projectDirFor(deps: ToolDeps, sessionId: string | undefined): string;
|
|
16
35
|
/**
|
|
17
36
|
* The connected session's projectId, or undefined when there is no session to ask.
|
|
18
37
|
*
|
|
19
|
-
* `sessions.resolve` throws
|
|
20
|
-
* several
|
|
21
|
-
*
|
|
38
|
+
* `sessions.resolve` throws for three reasons, and they do not all mean the same thing. Nothing
|
|
39
|
+
* connected, and several connected with none named, both mean "we cannot tell which project this
|
|
40
|
+
* call is about" — on a call that named no session that is not a reason to fail the caller's tool,
|
|
41
|
+
* which may not need a session at all, so it degrades to the daemon's own root.
|
|
42
|
+
*
|
|
43
|
+
* A sessionId that names NO session is the third, and it is a different fact. An agent that passes
|
|
44
|
+
* an id has named the project too, so swallowing that throw does not degrade to a cautious answer:
|
|
45
|
+
* it silently retargets the call at whichever checkout the daemon was started in (#994).
|
|
46
|
+
* The same fallback can expose another project's ledger on reads as well as misdirect writes.
|
|
47
|
+
*
|
|
48
|
+
* So the ambiguous case still declines (the resolver has its own `AMBIGUOUS` branch for the same
|
|
49
|
+
* reason) and the mis-addressed case refuses. There is no root that is right for a dead id, so
|
|
50
|
+
* there is no answer either.
|
|
22
51
|
*/
|
|
23
|
-
export declare function sessionProjectId(deps: ToolDeps, sessionId: string | undefined):
|
|
52
|
+
export declare function sessionProjectId(deps: ToolDeps, sessionId: string | undefined): ProjectId | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
1
2
|
/**
|
|
2
3
|
* The `.reticle` directory a tool call's artifacts belong in.
|
|
3
4
|
*
|
|
@@ -8,18 +9,62 @@
|
|
|
8
9
|
* fixed — the flow would save successfully and then be invisible to the tool that exists to replay
|
|
9
10
|
* it, which reads as "no flows covered this change" rather than as an error.
|
|
10
11
|
*
|
|
11
|
-
* Falls back to `deps.reticleRoot` whenever resolution cannot name a project
|
|
12
|
-
*
|
|
12
|
+
* Falls back to `deps.reticleRoot` whenever resolution cannot name a project — EXCEPT when the
|
|
13
|
+
* caller named a session that resolves to nothing, which is the one case where the fallback is a
|
|
14
|
+
* wrong answer rather than a cautious one. See `sessionProjectId`.
|
|
13
15
|
*/
|
|
14
16
|
export function sessionRoot(deps, sessionId) {
|
|
15
|
-
|
|
17
|
+
// Skip the LOOKUP, never the refusal.
|
|
18
|
+
//
|
|
19
|
+
// `artifactRootFor?.(…)` short-circuits its own argument, so an unwired caller used to skip
|
|
20
|
+
// resolution entirely and hand a named-but-dead session the daemon's own directory without ever
|
|
21
|
+
// checking the id -- which is how one app's intents, flows and runs land in another app's
|
|
22
|
+
// checkout. Resolving as an ARGUMENT fixes that, because arguments are evaluated first.
|
|
23
|
+
//
|
|
24
|
+
// But `resolve(undefined)` is not free: it builds the ranked no-session diagnosis and records
|
|
25
|
+
// which branch it took. With no resolver wired AND no id named there is nothing to refuse and
|
|
26
|
+
// nothing its answer could change, so that one case skips the lookup and costs what it always
|
|
27
|
+
// did. Every embedder of this engine, and every older construction of ToolDeps, is on it.
|
|
28
|
+
const nothingToRefuse = deps.artifactRootFor === undefined && sessionId === undefined;
|
|
29
|
+
return rootForProjectId(deps, nothingToRefuse ? undefined : sessionProjectId(deps, sessionId));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The same answer for a caller that already holds the projectId rather than a sessionId.
|
|
33
|
+
*
|
|
34
|
+
* The suite paths resolve the project ONCE and thread it down -- the run artifact, the cloud link,
|
|
35
|
+
* the flake ledger and the flows all belong to that one project -- so asking them to go back to the
|
|
36
|
+
* session manager for an id they already have is how a caller ends up taking the root from one
|
|
37
|
+
* place and the id from another. `sessionRoot` is now this function plus a lookup, so the two
|
|
38
|
+
* cannot disagree about what a resolved root is.
|
|
39
|
+
*/
|
|
40
|
+
export function rootForProjectId(deps, projectId) {
|
|
41
|
+
return deps.artifactRootFor?.(projectId).root ?? deps.reticleRoot;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The PROJECT directory -- one level above `.reticle` -- for a caller that runs a tool in a tree
|
|
45
|
+
* rather than writing a file into one. `git diff` is the case: it was being run in the daemon's
|
|
46
|
+
* `.reticle` subdirectory, which answers about the wrong repository (or, from a globally-registered
|
|
47
|
+
* daemon at `/` or `$HOME`, about none) while reporting a clean "nothing changed".
|
|
48
|
+
*/
|
|
49
|
+
export function projectDirFor(deps, sessionId) {
|
|
50
|
+
return dirname(sessionRoot(deps, sessionId));
|
|
16
51
|
}
|
|
17
52
|
/**
|
|
18
53
|
* The connected session's projectId, or undefined when there is no session to ask.
|
|
19
54
|
*
|
|
20
|
-
* `sessions.resolve` throws
|
|
21
|
-
* several
|
|
22
|
-
*
|
|
55
|
+
* `sessions.resolve` throws for three reasons, and they do not all mean the same thing. Nothing
|
|
56
|
+
* connected, and several connected with none named, both mean "we cannot tell which project this
|
|
57
|
+
* call is about" — on a call that named no session that is not a reason to fail the caller's tool,
|
|
58
|
+
* which may not need a session at all, so it degrades to the daemon's own root.
|
|
59
|
+
*
|
|
60
|
+
* A sessionId that names NO session is the third, and it is a different fact. An agent that passes
|
|
61
|
+
* an id has named the project too, so swallowing that throw does not degrade to a cautious answer:
|
|
62
|
+
* it silently retargets the call at whichever checkout the daemon was started in (#994).
|
|
63
|
+
* The same fallback can expose another project's ledger on reads as well as misdirect writes.
|
|
64
|
+
*
|
|
65
|
+
* So the ambiguous case still declines (the resolver has its own `AMBIGUOUS` branch for the same
|
|
66
|
+
* reason) and the mis-addressed case refuses. There is no root that is right for a dead id, so
|
|
67
|
+
* there is no answer either.
|
|
23
68
|
*/
|
|
24
69
|
/*
|
|
25
70
|
* Exported alongside `sessionRoot`, and that is the point rather than a convenience. A caller that
|
|
@@ -32,7 +77,24 @@ export function sessionProjectId(deps, sessionId) {
|
|
|
32
77
|
try {
|
|
33
78
|
return deps.sessions.resolve(sessionId).projectId;
|
|
34
79
|
}
|
|
35
|
-
catch {
|
|
36
|
-
|
|
80
|
+
catch (cause) {
|
|
81
|
+
if (sessionId === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
throw unresolvedTargetRefusal(sessionId, cause);
|
|
37
84
|
}
|
|
38
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Why a named-but-dead session gets no directory, and what to do instead.
|
|
88
|
+
*
|
|
89
|
+
* The manager's own message is carried verbatim at the end because it is the actionable half — it
|
|
90
|
+
* names the sessions that ARE connected, with their ids and urls, or says that none are. Rebuilding
|
|
91
|
+
* a worse version of that here is how the two no-session paths drifted apart the last time.
|
|
92
|
+
*/
|
|
93
|
+
function unresolvedTargetRefusal(sessionId, cause) {
|
|
94
|
+
const because = cause instanceof Error ? cause.message : String(cause);
|
|
95
|
+
return new Error(`sessionId '${sessionId}' names no session, so there is no project to resolve this call's ` +
|
|
96
|
+
'.reticle directory against, and nothing was read or written. Reticle refuses rather than ' +
|
|
97
|
+
"falling back to the daemon's own directory, which belongs to whatever project the daemon " +
|
|
98
|
+
"was started in — that is how one app's intents, flows and runs land in another app's " +
|
|
99
|
+
`checkout. Omit sessionId to use the connected app, or name one that is live. ${because}`);
|
|
100
|
+
}
|
|
@@ -20,4 +20,13 @@
|
|
|
20
20
|
* With no evidence — a fresh daemon, or a HELLO naming no project — nothing is inferred and the
|
|
21
21
|
* plain reason stands, unless the page sent no token.
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* Whether a recorded close is a pairing-token refusal.
|
|
25
|
+
*
|
|
26
|
+
* The bridge records the specific sentence it sent on the socket, not one fixed string, so a
|
|
27
|
+
* comparison with a single constant misses a wrong token, a missing token, and a daemon that
|
|
28
|
+
* belongs to another project. All three are refusals. A later good session is a separate question
|
|
29
|
+
* the caller answers with `connectedSinceLastClosure`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isAuthRefusalReason(reason: string | undefined): boolean;
|
|
23
32
|
export declare function authFailureReason(servedProjects: ReadonlySet<string>, helloProject: string | undefined, helloToken?: string): string;
|
|
@@ -32,6 +32,20 @@ const WRONG_TOKEN = 'authentication failed: wrong pairing token — run `reticle
|
|
|
32
32
|
const NO_TOKEN = 'authentication failed: no pairing token on the page';
|
|
33
33
|
/** WebSocket close reasons are capped at 123 bytes; a longer one throws and closes with nothing. */
|
|
34
34
|
const MAX_REASON_BYTES = 123;
|
|
35
|
+
const DIFFERENT_PROJECT_PREFIX = 'this daemon serves a different project';
|
|
36
|
+
/**
|
|
37
|
+
* Whether a recorded close is a pairing-token refusal.
|
|
38
|
+
*
|
|
39
|
+
* The bridge records the specific sentence it sent on the socket, not one fixed string, so a
|
|
40
|
+
* comparison with a single constant misses a wrong token, a missing token, and a daemon that
|
|
41
|
+
* belongs to another project. All three are refusals. A later good session is a separate question
|
|
42
|
+
* the caller answers with `connectedSinceLastClosure`.
|
|
43
|
+
*/
|
|
44
|
+
export function isAuthRefusalReason(reason) {
|
|
45
|
+
if (reason === undefined)
|
|
46
|
+
return false;
|
|
47
|
+
return reason.startsWith('authentication failed') || reason.startsWith(DIFFERENT_PROJECT_PREFIX);
|
|
48
|
+
}
|
|
35
49
|
export function authFailureReason(servedProjects, helloProject, helloToken) {
|
|
36
50
|
// ONE served project, not merely some. A daemon that has accepted sessions from SEVERAL projects
|
|
37
51
|
// is demonstrably not owned by any one of them, so "that daemon is not yours, stop it" is the
|
|
@@ -45,7 +59,7 @@ export function authFailureReason(servedProjects, helloProject, helloToken) {
|
|
|
45
59
|
if (helloProject !== undefined &&
|
|
46
60
|
1 === servedProjects.size &&
|
|
47
61
|
!servedProjects.has(helloProject)) {
|
|
48
|
-
const reason =
|
|
62
|
+
const reason = `${DIFFERENT_PROJECT_PREFIX} — run \`reticle stop\` and retry`;
|
|
49
63
|
return Buffer.byteLength(reason, 'utf8') <= MAX_REASON_BYTES ? reason : PLAIN;
|
|
50
64
|
}
|
|
51
65
|
if (helloToken === undefined || 0 === helloToken.length)
|
|
@@ -20,7 +20,7 @@ export declare const WS_CLOSE_REASON: {
|
|
|
20
20
|
* this stays as the close-code table's label and as the fallback when the skew cannot be read.
|
|
21
21
|
*/
|
|
22
22
|
readonly PROTOCOL_MISMATCH: "protocol version mismatch — @reticlehq/browser and server disagree";
|
|
23
|
-
readonly AUTH_FAILED: "authentication failed —
|
|
23
|
+
readonly AUTH_FAILED: "authentication failed — restart the dev server; a reload keeps the old pairing token";
|
|
24
24
|
/**
|
|
25
25
|
* The daemon threw while handling a message. Closing is deliberate: an exception leaves the
|
|
26
26
|
* session in a state neither side can describe, and a socket left OPEN and unresponsive is the
|
|
@@ -97,6 +97,11 @@ export declare class Bridge {
|
|
|
97
97
|
* is to make something happen sooner than it already would.
|
|
98
98
|
*/
|
|
99
99
|
attachSyncRequest(handler: () => void): void;
|
|
100
|
+
/**
|
|
101
|
+
* Register a handler for the panel's harness switch. Optional for the same reason as the above: a
|
|
102
|
+
* bridge built without one ignores the request rather than refusing it.
|
|
103
|
+
*/
|
|
104
|
+
attachHarnessRequest(handler: (enabled: boolean) => void): void;
|
|
100
105
|
/** Register a handler to run when a session connects. Additive — every handler runs. */
|
|
101
106
|
attachSessionCreate(handler: (session: Session) => void): void;
|
|
102
107
|
close(): Promise<void>;
|
|
@@ -48,7 +48,7 @@ export const WS_CLOSE_REASON = {
|
|
|
48
48
|
* this stays as the close-code table's label and as the fallback when the skew cannot be read.
|
|
49
49
|
*/
|
|
50
50
|
PROTOCOL_MISMATCH: 'protocol version mismatch — @reticlehq/browser and server disagree',
|
|
51
|
-
AUTH_FAILED: 'authentication failed —
|
|
51
|
+
AUTH_FAILED: 'authentication failed — restart the dev server; a reload keeps the old pairing token',
|
|
52
52
|
/**
|
|
53
53
|
* The daemon threw while handling a message. Closing is deliberate: an exception leaves the
|
|
54
54
|
* session in a state neither side can describe, and a socket left OPEN and unresponsive is the
|
|
@@ -119,6 +119,24 @@ function replayRequest(event) {
|
|
|
119
119
|
const name = event.data['text'];
|
|
120
120
|
return 'string' === typeof name && name.length > 0 ? name : undefined;
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The desired harness state if this event is the panel's switch, else undefined.
|
|
124
|
+
*
|
|
125
|
+
* Carries `on`/`off` rather than meaning "toggle": a double-click on a flaky connection sends two,
|
|
126
|
+
* and two toggles land back where they started while two `on`s are the same as one.
|
|
127
|
+
*/
|
|
128
|
+
function harnessRequest(event) {
|
|
129
|
+
if (event.type !== EventType.HUMAN_CONTROL)
|
|
130
|
+
return undefined;
|
|
131
|
+
if (event.data['kind'] !== HumanControlKind.HARNESS)
|
|
132
|
+
return undefined;
|
|
133
|
+
const want = event.data['text'];
|
|
134
|
+
if ('on' === want)
|
|
135
|
+
return true;
|
|
136
|
+
if ('off' === want)
|
|
137
|
+
return false;
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
122
140
|
/** True when this event is the panel's "sync now" button. Pure boundary narrowing, like the above. */
|
|
123
141
|
function isSyncRequest(event) {
|
|
124
142
|
return event.type === EventType.HUMAN_CONTROL && event.data['kind'] === HumanControlKind.SYNC;
|
|
@@ -225,6 +243,7 @@ export class Bridge {
|
|
|
225
243
|
#onSessionEnd;
|
|
226
244
|
/** Wired by the daemon: push to the dashboard now, because somebody asked in the panel. */
|
|
227
245
|
#onSyncRequest;
|
|
246
|
+
#onHarnessRequest;
|
|
228
247
|
constructor(options) {
|
|
229
248
|
const host = options.host ?? LOOPBACK_HOST;
|
|
230
249
|
if ((options.token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
|
|
@@ -479,7 +498,7 @@ export class Bridge {
|
|
|
479
498
|
presented: parsed.token !== undefined && 0 < parsed.token.length,
|
|
480
499
|
tokenSource: this.#tokenSource,
|
|
481
500
|
});
|
|
482
|
-
this.sessions.noteClosure(
|
|
501
|
+
this.sessions.noteClosure(reason, this.#clock());
|
|
483
502
|
socket.close(WS_CLOSE.AUTH_FAILED[0], reason);
|
|
484
503
|
return;
|
|
485
504
|
}
|
|
@@ -616,6 +635,10 @@ export class Bridge {
|
|
|
616
635
|
// A panel ▶ replay needs the daemon's flow store, which the Session can't reach — route it to
|
|
617
636
|
// the daemon-wired handler instead of the in-session control path. Everything else is normal.
|
|
618
637
|
const replay = replayRequest(parsed.event);
|
|
638
|
+
// Narrowed before the chain, not inside it: the result is needed by the branch AND by the
|
|
639
|
+
// test that decides whether this is a panel control at all, and calling it twice would
|
|
640
|
+
// invite the two calls to disagree.
|
|
641
|
+
const harness = harnessRequest(parsed.event);
|
|
619
642
|
if (replay !== undefined)
|
|
620
643
|
this.#onReplay?.(session.id, replay);
|
|
621
644
|
// Same shape as ▶ replay, and routed the same way: what to push lives in the daemon's
|
|
@@ -623,6 +646,10 @@ export class Bridge {
|
|
|
623
646
|
// us to hurry, not asking for a result.
|
|
624
647
|
else if (isSyncRequest(parsed.event))
|
|
625
648
|
this.#onSyncRequest?.();
|
|
649
|
+
// The switch is written through to the platform by the daemon, for the same reason: the
|
|
650
|
+
// Session has no cloud credential and should not grow one.
|
|
651
|
+
else if (harness !== undefined)
|
|
652
|
+
this.#onHarnessRequest?.(harness);
|
|
626
653
|
// Pass the raw frame's byte length so the buffer doesn't re-serialize every event for accounting.
|
|
627
654
|
else
|
|
628
655
|
session.pushEvent(parsed.event, Buffer.byteLength(text, 'utf8'));
|
|
@@ -758,6 +785,13 @@ export class Bridge {
|
|
|
758
785
|
attachSyncRequest(handler) {
|
|
759
786
|
this.#onSyncRequest = handler;
|
|
760
787
|
}
|
|
788
|
+
/**
|
|
789
|
+
* Register a handler for the panel's harness switch. Optional for the same reason as the above: a
|
|
790
|
+
* bridge built without one ignores the request rather than refusing it.
|
|
791
|
+
*/
|
|
792
|
+
attachHarnessRequest(handler) {
|
|
793
|
+
this.#onHarnessRequest = handler;
|
|
794
|
+
}
|
|
761
795
|
/** Register a handler to run when a session connects. Additive — every handler runs. */
|
|
762
796
|
attachSessionCreate(handler) {
|
|
763
797
|
this.#onSessionCreate.push(handler);
|
|
@@ -22,3 +22,22 @@ export declare function sessionReplacedReason(id: string, url: string): string;
|
|
|
22
22
|
* without each one re-deriving a message — an `Error`, a bare string, or something else entirely.
|
|
23
23
|
*/
|
|
24
24
|
export declare function isSessionReplacedError(error: unknown): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The reason every command still in flight is rejected with when the socket under it closes.
|
|
27
|
+
*
|
|
28
|
+
* A full-document navigation closes that socket, so on an MPA — or on any click that loads a new
|
|
29
|
+
* page — this arrives for the exact command that CAUSED it, and the successor is still connecting.
|
|
30
|
+
* `SessionManager.remove` cannot tell those two apart at the moment it runs: it is called from the
|
|
31
|
+
* socket's own close handler, the tombstone it writes is what a successor is later matched against,
|
|
32
|
+
* and nothing has arrived yet. So the rejection stays a plain transport failure, as
|
|
33
|
+
* `PendingCommands.rejectAll` documents, and a caller that CAN wait decides what it means.
|
|
34
|
+
*/
|
|
35
|
+
export declare const SESSION_DISCONNECTED_REASON = "session disconnected";
|
|
36
|
+
/**
|
|
37
|
+
* Is this failure the page's transport going away, rather than an answer from the page?
|
|
38
|
+
*
|
|
39
|
+
* True for both shapes the same event takes: the reconnect that claimed the id back, and the close
|
|
40
|
+
* that landed before any successor did. A caller holding a budget can follow either; a caller that
|
|
41
|
+
* cannot must say it stopped watching, never that the app failed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isDocumentGoneError(error: unknown): boolean;
|
|
@@ -27,3 +27,25 @@ export function isSessionReplacedError(error) {
|
|
|
27
27
|
const message = error instanceof Error ? error.message : String(error);
|
|
28
28
|
return message.includes(SESSION_REPLACED_PREFIX);
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* The reason every command still in flight is rejected with when the socket under it closes.
|
|
32
|
+
*
|
|
33
|
+
* A full-document navigation closes that socket, so on an MPA — or on any click that loads a new
|
|
34
|
+
* page — this arrives for the exact command that CAUSED it, and the successor is still connecting.
|
|
35
|
+
* `SessionManager.remove` cannot tell those two apart at the moment it runs: it is called from the
|
|
36
|
+
* socket's own close handler, the tombstone it writes is what a successor is later matched against,
|
|
37
|
+
* and nothing has arrived yet. So the rejection stays a plain transport failure, as
|
|
38
|
+
* `PendingCommands.rejectAll` documents, and a caller that CAN wait decides what it means.
|
|
39
|
+
*/
|
|
40
|
+
export const SESSION_DISCONNECTED_REASON = 'session disconnected';
|
|
41
|
+
/**
|
|
42
|
+
* Is this failure the page's transport going away, rather than an answer from the page?
|
|
43
|
+
*
|
|
44
|
+
* True for both shapes the same event takes: the reconnect that claimed the id back, and the close
|
|
45
|
+
* that landed before any successor did. A caller holding a budget can follow either; a caller that
|
|
46
|
+
* cannot must say it stopped watching, never that the app failed.
|
|
47
|
+
*/
|
|
48
|
+
export function isDocumentGoneError(error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
return isSessionReplacedError(error) || message === SESSION_DISCONNECTED_REASON;
|
|
51
|
+
}
|
|
@@ -165,6 +165,7 @@ export declare const SELF_RECOVERING_MARKER = "Then call reticle_sessions again
|
|
|
165
165
|
export declare function explainNoSession(facts: NoSessionFacts): {
|
|
166
166
|
reason: NoSessionReason;
|
|
167
167
|
message: string;
|
|
168
|
+
detail?: string;
|
|
168
169
|
};
|
|
169
170
|
/**
|
|
170
171
|
* The message alone, for the callers that render it.
|