@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,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness driver for OpenAI's Chat Completions API.
|
|
3
|
+
*
|
|
4
|
+
* Third driver, and the reason it exists is comparison rather than capability: people want to know
|
|
5
|
+
* whether the cheap typed-decision model is good enough for THEIR app, and "cheap versus one
|
|
6
|
+
* frontier model" is a weaker answer than "cheap versus the two they might otherwise reach for".
|
|
7
|
+
*
|
|
8
|
+
* A translation layer, exactly like `driver.ts` — the loop lives in `harness.ts` against
|
|
9
|
+
* `ModelDriver` and knows nothing about any wire. Chat Completions is a genuinely different shape
|
|
10
|
+
* from the Messages API, and translating OpenAI into Anthropic's wire on the platform side was
|
|
11
|
+
* considered and rejected there: it would put a second, undeclared model-shaped transformation
|
|
12
|
+
* inside the thing being measured. So this speaks the native format and the arm measures OpenAI.
|
|
13
|
+
*
|
|
14
|
+
* NOT cache-annotated, and that is a real asymmetry to hold in mind when reading a cost comparison
|
|
15
|
+
* against the Anthropic arm. OpenAI caches long prefixes automatically and reports it back in
|
|
16
|
+
* `usage.prompt_tokens_details.cached_tokens`, so the saving is not something this driver arranges;
|
|
17
|
+
* it is reported below so the number is visible rather than assumed.
|
|
18
|
+
*/
|
|
19
|
+
import type { ModelDriver } from './harness.js';
|
|
20
|
+
import type { HarnessFetch } from './driver.js';
|
|
21
|
+
/**
|
|
22
|
+
* The default model.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately not the largest available, for the same reason `DEFAULT_HARNESS_MODEL` is not: the
|
|
25
|
+
* driver is choosing what to touch next, and the engine decides the verdict from evidence. A driver
|
|
26
|
+
* that needed a frontier model would be evidence the engine was not doing its job.
|
|
27
|
+
*/
|
|
28
|
+
export declare const DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
|
|
29
|
+
export declare const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
|
|
30
|
+
export interface OpenAiDriverOptions {
|
|
31
|
+
apiKey: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
baseUrl?: string;
|
|
34
|
+
/** Injected for tests. Defaults to the platform `fetch`. */
|
|
35
|
+
fetch?: HarnessFetch;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Read this driver's configuration out of the environment.
|
|
39
|
+
*
|
|
40
|
+
* A real OpenAI key goes to OpenAI. The platform key goes to the platform, which forwards with its
|
|
41
|
+
* own — so somebody who has run `reticle link` can drive with this model having never held an
|
|
42
|
+
* OpenAI key, which is the entire point of the proxy.
|
|
43
|
+
*/
|
|
44
|
+
export declare function openAiOptionsFromEnv(env: Record<string, string | undefined>): OpenAiDriverOptions | undefined;
|
|
45
|
+
/** Build a driver backed by Chat Completions. */
|
|
46
|
+
export declare function openAiDriver(options: OpenAiDriverOptions): ModelDriver;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness driver for OpenAI's Chat Completions API.
|
|
3
|
+
*
|
|
4
|
+
* Third driver, and the reason it exists is comparison rather than capability: people want to know
|
|
5
|
+
* whether the cheap typed-decision model is good enough for THEIR app, and "cheap versus one
|
|
6
|
+
* frontier model" is a weaker answer than "cheap versus the two they might otherwise reach for".
|
|
7
|
+
*
|
|
8
|
+
* A translation layer, exactly like `driver.ts` — the loop lives in `harness.ts` against
|
|
9
|
+
* `ModelDriver` and knows nothing about any wire. Chat Completions is a genuinely different shape
|
|
10
|
+
* from the Messages API, and translating OpenAI into Anthropic's wire on the platform side was
|
|
11
|
+
* considered and rejected there: it would put a second, undeclared model-shaped transformation
|
|
12
|
+
* inside the thing being measured. So this speaks the native format and the arm measures OpenAI.
|
|
13
|
+
*
|
|
14
|
+
* NOT cache-annotated, and that is a real asymmetry to hold in mind when reading a cost comparison
|
|
15
|
+
* against the Anthropic arm. OpenAI caches long prefixes automatically and reports it back in
|
|
16
|
+
* `usage.prompt_tokens_details.cached_tokens`, so the saving is not something this driver arranges;
|
|
17
|
+
* it is reported below so the number is visible rather than assumed.
|
|
18
|
+
*/
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import { ReticleEnv, apiKeyFrom } from '@reticlehq/core';
|
|
21
|
+
/**
|
|
22
|
+
* The default model.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately not the largest available, for the same reason `DEFAULT_HARNESS_MODEL` is not: the
|
|
25
|
+
* driver is choosing what to touch next, and the engine decides the verdict from evidence. A driver
|
|
26
|
+
* that needed a frontier model would be evidence the engine was not doing its job.
|
|
27
|
+
*/
|
|
28
|
+
export const DEFAULT_OPENAI_MODEL = 'gpt-5.6-luna';
|
|
29
|
+
export const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com';
|
|
30
|
+
/** The platform's own path, which forwards with Reticle's key rather than the caller's. */
|
|
31
|
+
const PLATFORM_PATH = '/v1/model/openai';
|
|
32
|
+
const OPENAI_PATH = '/v1/chat/completions';
|
|
33
|
+
/** Output cap per turn. A driving turn is a sentence and a tool call, never an essay. */
|
|
34
|
+
const MAX_TOKENS = 4096;
|
|
35
|
+
/** How much of one tool result the model is allowed to see. Same reasoning as the Anthropic driver. */
|
|
36
|
+
const MAX_RESULT_CHARS = 8000;
|
|
37
|
+
const ToolCall = z.object({
|
|
38
|
+
id: z.string(),
|
|
39
|
+
function: z.object({ name: z.string(), arguments: z.string().default('{}') }),
|
|
40
|
+
});
|
|
41
|
+
const ChatResponse = z.object({
|
|
42
|
+
choices: z
|
|
43
|
+
.array(z.object({
|
|
44
|
+
message: z.object({
|
|
45
|
+
content: z.string().nullish(),
|
|
46
|
+
tool_calls: z.array(ToolCall).nullish(),
|
|
47
|
+
}),
|
|
48
|
+
}))
|
|
49
|
+
.default([]),
|
|
50
|
+
usage: z
|
|
51
|
+
.object({
|
|
52
|
+
prompt_tokens: z.number().default(0),
|
|
53
|
+
completion_tokens: z.number().default(0),
|
|
54
|
+
prompt_tokens_details: z.object({ cached_tokens: z.number().nullish() }).nullish(),
|
|
55
|
+
})
|
|
56
|
+
.default({ prompt_tokens: 0, completion_tokens: 0 }),
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* Read this driver's configuration out of the environment.
|
|
60
|
+
*
|
|
61
|
+
* A real OpenAI key goes to OpenAI. The platform key goes to the platform, which forwards with its
|
|
62
|
+
* own — so somebody who has run `reticle link` can drive with this model having never held an
|
|
63
|
+
* OpenAI key, which is the entire point of the proxy.
|
|
64
|
+
*/
|
|
65
|
+
export function openAiOptionsFromEnv(env) {
|
|
66
|
+
const model = env[ReticleEnv.HARNESS_OPENAI_MODEL];
|
|
67
|
+
const withModel = (rest) => ({
|
|
68
|
+
...rest,
|
|
69
|
+
...(model === undefined || 0 === model.length ? {} : { model }),
|
|
70
|
+
});
|
|
71
|
+
const direct = env[ReticleEnv.HARNESS_OPENAI_KEY];
|
|
72
|
+
if (direct !== undefined && 0 < direct.length)
|
|
73
|
+
return withModel({ apiKey: direct });
|
|
74
|
+
const cloudKey = apiKeyFrom(env);
|
|
75
|
+
const cloudUrl = env[ReticleEnv.CLOUD_URL];
|
|
76
|
+
if (cloudKey === undefined || 0 === cloudKey.length)
|
|
77
|
+
return undefined;
|
|
78
|
+
if (cloudUrl === undefined || 0 === cloudUrl.length)
|
|
79
|
+
return undefined;
|
|
80
|
+
return withModel({ apiKey: cloudKey, baseUrl: cloudUrl });
|
|
81
|
+
}
|
|
82
|
+
function toWireTool(tool) {
|
|
83
|
+
return {
|
|
84
|
+
type: 'function',
|
|
85
|
+
function: { name: tool.name, description: tool.description, parameters: tool.inputSchema },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function truncate(text) {
|
|
89
|
+
return text.length <= MAX_RESULT_CHARS
|
|
90
|
+
? text
|
|
91
|
+
: `${text.slice(0, MAX_RESULT_CHARS)}\n…[truncated: ${String(text.length - MAX_RESULT_CHARS)} more characters]`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The history, in Chat Completions' shape.
|
|
95
|
+
*
|
|
96
|
+
* The one structural difference from the Messages API, and it matters: a tool result is its OWN
|
|
97
|
+
* message carrying `tool_call_id`, where Anthropic puts every result for a turn into one user
|
|
98
|
+
* message. So a turn with three parallel calls becomes one assistant message and three tool
|
|
99
|
+
* messages. That is the format's requirement rather than a choice — an unanswered `tool_call_id`
|
|
100
|
+
* is rejected outright, so every outcome must be emitted, including the failures.
|
|
101
|
+
*/
|
|
102
|
+
function toWireMessages(history) {
|
|
103
|
+
const messages = [];
|
|
104
|
+
for (const entry of history) {
|
|
105
|
+
if ('user' === entry.role) {
|
|
106
|
+
messages.push({ role: 'user', content: entry.text });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if ('assistant' === entry.role) {
|
|
110
|
+
messages.push({
|
|
111
|
+
role: 'assistant',
|
|
112
|
+
content: 0 === entry.text.length ? null : entry.text,
|
|
113
|
+
...(0 === entry.calls.length
|
|
114
|
+
? {}
|
|
115
|
+
: {
|
|
116
|
+
tool_calls: entry.calls.map((call) => ({
|
|
117
|
+
id: call.id,
|
|
118
|
+
type: 'function',
|
|
119
|
+
function: { name: call.name, arguments: JSON.stringify(call.args) },
|
|
120
|
+
})),
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const outcome of entry.outcomes) {
|
|
126
|
+
messages.push({
|
|
127
|
+
role: 'tool',
|
|
128
|
+
tool_call_id: outcome.id,
|
|
129
|
+
content: truncate(JSON.stringify(outcome.result)),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return messages;
|
|
134
|
+
}
|
|
135
|
+
/** Arguments arrive as a JSON STRING here, not an object — a malformed one is empty, never a throw. */
|
|
136
|
+
function parseArgs(raw) {
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(raw);
|
|
139
|
+
return 'object' === typeof parsed && null !== parsed ? parsed : {};
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Build a driver backed by Chat Completions. */
|
|
146
|
+
export function openAiDriver(options) {
|
|
147
|
+
const model = options.model ?? DEFAULT_OPENAI_MODEL;
|
|
148
|
+
const baseUrl = options.baseUrl ?? DEFAULT_OPENAI_BASE_URL;
|
|
149
|
+
const doFetch = options.fetch ?? ((url, init) => fetch(url, init));
|
|
150
|
+
// The platform does not expose `/v1/chat/completions`; it exposes its own path and forwards. A
|
|
151
|
+
// base URL that is not OpenAI's is the platform's, which is the only other thing this talks to.
|
|
152
|
+
const path = DEFAULT_OPENAI_BASE_URL === baseUrl ? OPENAI_PATH : PLATFORM_PATH;
|
|
153
|
+
return {
|
|
154
|
+
async turn(input) {
|
|
155
|
+
const response = await doFetch(`${baseUrl}${path}`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: {
|
|
158
|
+
'content-type': 'application/json',
|
|
159
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
model,
|
|
163
|
+
max_completion_tokens: MAX_TOKENS,
|
|
164
|
+
tools: input.tools.map(toWireTool),
|
|
165
|
+
messages: [{ role: 'system', content: input.system }, ...toWireMessages(input.history)],
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
const body = await response.text();
|
|
169
|
+
// Thrown, not returned: a model that will not answer is a BROKEN drive, and a broken drive is
|
|
170
|
+
// never reported as clean. Swallowing it would be a verdict about an app nobody drove.
|
|
171
|
+
if (!response.ok)
|
|
172
|
+
throw new Error(`the model refused the request (HTTP ${String(response.status)}): ${body}`);
|
|
173
|
+
const parsed = ChatResponse.parse(JSON.parse(body));
|
|
174
|
+
const message = parsed.choices[0]?.message;
|
|
175
|
+
const calls = (message?.tool_calls ?? []).map((call) => ({
|
|
176
|
+
id: call.id,
|
|
177
|
+
name: call.function.name,
|
|
178
|
+
args: parseArgs(call.function.arguments),
|
|
179
|
+
}));
|
|
180
|
+
const cached = parsed.usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
181
|
+
return {
|
|
182
|
+
text: message?.content ?? '',
|
|
183
|
+
calls,
|
|
184
|
+
usage: {
|
|
185
|
+
// `prompt_tokens` INCLUDES the cached ones here, unlike the Messages API where the two are
|
|
186
|
+
// disjoint. Subtracting keeps `input` meaning the same thing in both drivers: what was
|
|
187
|
+
// processed at full price. A cost comparison that summed them would charge the arm that
|
|
188
|
+
// cached well for the tokens it did not pay full price for.
|
|
189
|
+
input: Math.max(parsed.usage.prompt_tokens - cached, 0),
|
|
190
|
+
output: parsed.usage.completion_tokens,
|
|
191
|
+
cacheRead: cached,
|
|
192
|
+
cacheWrite: 0,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The driver preference a person set on the platform, read by the daemon that has to honour it.
|
|
3
|
+
*
|
|
4
|
+
* Choosing a model in a web UI is worth nothing if the thing doing the driving never asks. This is
|
|
5
|
+
* the one call that closes that loop: the console writes a per-project preference, and the daemon
|
|
6
|
+
* reads it before it builds a driver.
|
|
7
|
+
*
|
|
8
|
+
* Three rules shape all of it, and they are the difference between a preference and an outage:
|
|
9
|
+
*
|
|
10
|
+
* - it is a PREFERENCE, not a credential. Every failure — no key, no host, a timeout, a 500, a
|
|
11
|
+
* body that does not parse — answers `undefined`, and the caller falls back to what the
|
|
12
|
+
* environment says. A drive must never fail because a settings endpoint was slow.
|
|
13
|
+
* - it is BOUNDED. The fetch is aborted rather than awaited indefinitely. A drive that hangs
|
|
14
|
+
* holds a leased browser context with nothing anywhere saying why, and "the preferences API was
|
|
15
|
+
* unreachable" is the worst possible reason for that to happen.
|
|
16
|
+
* - it never overrides someone who was EXPLICIT. A driver named in the call, or set in the
|
|
17
|
+
* environment, wins over the stored preference. Precedence lives in the caller.
|
|
18
|
+
*/
|
|
19
|
+
/** The subset of the platform's answer the harness acts on. */
|
|
20
|
+
export interface PlatformModelConfig {
|
|
21
|
+
/** `jev` | `anthropic` | `openai`, as the platform spells it. Not validated here. */
|
|
22
|
+
provider: string;
|
|
23
|
+
/** Whether the person wants autonomous driving at all. */
|
|
24
|
+
harnessEnabled: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Whether this workspace may drive on OUR model spend — a claimed free period, or a paid plan.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately a second boolean rather than folded into `harnessEnabled`. One is a switch a person
|
|
29
|
+
* set, the other is a fact about their account, and a daemon that could not tell them apart would
|
|
30
|
+
* report "you turned this off" to somebody whose free months quietly ran out.
|
|
31
|
+
*/
|
|
32
|
+
harnessEntitled: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** A GET, narrowed to what this file uses, so a test can answer it without a network. */
|
|
35
|
+
export type ConfigFetch = (url: string, init: {
|
|
36
|
+
method: string;
|
|
37
|
+
headers: Record<string, string>;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}) => Promise<{
|
|
40
|
+
ok: boolean;
|
|
41
|
+
status: number;
|
|
42
|
+
text(): Promise<string>;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Ask the platform which model this project wants driving with.
|
|
46
|
+
*
|
|
47
|
+
* `undefined` means "no answer", and every caller must treat that as "carry on with the
|
|
48
|
+
* environment" rather than as a failure worth reporting. There is deliberately no error channel:
|
|
49
|
+
* an unreachable settings endpoint is not something the person running a drive can act on, and a
|
|
50
|
+
* warning printed on a path that then works correctly is how people learn to ignore stderr.
|
|
51
|
+
*/
|
|
52
|
+
export declare function fetchPlatformConfig(env: Record<string, string | undefined>, doFetch?: ConfigFetch, timeoutMs?: number): Promise<PlatformModelConfig | undefined>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The driver preference a person set on the platform, read by the daemon that has to honour it.
|
|
3
|
+
*
|
|
4
|
+
* Choosing a model in a web UI is worth nothing if the thing doing the driving never asks. This is
|
|
5
|
+
* the one call that closes that loop: the console writes a per-project preference, and the daemon
|
|
6
|
+
* reads it before it builds a driver.
|
|
7
|
+
*
|
|
8
|
+
* Three rules shape all of it, and they are the difference between a preference and an outage:
|
|
9
|
+
*
|
|
10
|
+
* - it is a PREFERENCE, not a credential. Every failure — no key, no host, a timeout, a 500, a
|
|
11
|
+
* body that does not parse — answers `undefined`, and the caller falls back to what the
|
|
12
|
+
* environment says. A drive must never fail because a settings endpoint was slow.
|
|
13
|
+
* - it is BOUNDED. The fetch is aborted rather than awaited indefinitely. A drive that hangs
|
|
14
|
+
* holds a leased browser context with nothing anywhere saying why, and "the preferences API was
|
|
15
|
+
* unreachable" is the worst possible reason for that to happen.
|
|
16
|
+
* - it never overrides someone who was EXPLICIT. A driver named in the call, or set in the
|
|
17
|
+
* environment, wins over the stored preference. Precedence lives in the caller.
|
|
18
|
+
*/
|
|
19
|
+
import { ReticleEnv, apiKeyFrom } from '@reticlehq/core';
|
|
20
|
+
const CONFIG_PATH = '/v1/model/config';
|
|
21
|
+
/**
|
|
22
|
+
* How long the daemon will wait for a preference before driving without it.
|
|
23
|
+
*
|
|
24
|
+
* Short on purpose. This runs before a drive that takes tens of seconds, so a second of budget is
|
|
25
|
+
* affordable and a stall is not — and the fallback is not a degraded mode, it is the behaviour
|
|
26
|
+
* everybody had before this endpoint existed.
|
|
27
|
+
*/
|
|
28
|
+
const TIMEOUT_MS = 2_000;
|
|
29
|
+
function parse(body) {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(body);
|
|
32
|
+
if ('object' !== typeof parsed || null === parsed)
|
|
33
|
+
return undefined;
|
|
34
|
+
const record = parsed;
|
|
35
|
+
const provider = record['provider'];
|
|
36
|
+
if ('string' !== typeof provider || 0 === provider.length)
|
|
37
|
+
return undefined;
|
|
38
|
+
// Absent reads as enabled: the platform's own default is on, and a daemon that treated a
|
|
39
|
+
// missing field as "off" would silently disable the feature for anyone on an older API.
|
|
40
|
+
const enabled = record['harnessEnabled'];
|
|
41
|
+
// Same rule for both, for the same reason: an older API that reports neither must not have its
|
|
42
|
+
// silence read as a refusal.
|
|
43
|
+
const entitled = record['harnessEntitled'];
|
|
44
|
+
return {
|
|
45
|
+
provider,
|
|
46
|
+
harnessEnabled: 'boolean' === typeof enabled ? enabled : true,
|
|
47
|
+
harnessEntitled: 'boolean' === typeof entitled ? entitled : true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Ask the platform which model this project wants driving with.
|
|
56
|
+
*
|
|
57
|
+
* `undefined` means "no answer", and every caller must treat that as "carry on with the
|
|
58
|
+
* environment" rather than as a failure worth reporting. There is deliberately no error channel:
|
|
59
|
+
* an unreachable settings endpoint is not something the person running a drive can act on, and a
|
|
60
|
+
* warning printed on a path that then works correctly is how people learn to ignore stderr.
|
|
61
|
+
*/
|
|
62
|
+
export async function fetchPlatformConfig(env, doFetch = (url, init) => fetch(url, init), timeoutMs = TIMEOUT_MS) {
|
|
63
|
+
const key = apiKeyFrom(env);
|
|
64
|
+
const host = env[ReticleEnv.CLOUD_URL];
|
|
65
|
+
if (key === undefined || 0 === key.length)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (host === undefined || 0 === host.length)
|
|
68
|
+
return undefined;
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
controller.abort();
|
|
72
|
+
}, timeoutMs);
|
|
73
|
+
try {
|
|
74
|
+
const res = await doFetch(`${host.replace(/\/+$/, '')}${CONFIG_PATH}`, {
|
|
75
|
+
method: 'GET',
|
|
76
|
+
headers: { authorization: `Bearer ${key}` },
|
|
77
|
+
signal: controller.signal,
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
return undefined;
|
|
81
|
+
return parse(await res.text());
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -6,6 +6,21 @@ import { ratioSchema } from '../../surface/tools/args/numeric-bounds.js';
|
|
|
6
6
|
import { asNumber, asRecord, asString } from '@reticlehq/core';
|
|
7
7
|
import { diffPng } from './visual-diff.js';
|
|
8
8
|
import { VisualStore } from './visual-store.js';
|
|
9
|
+
/**
|
|
10
|
+
* The `.reticle` a capture belongs beside: the driven session's own, stamped when it connected.
|
|
11
|
+
*
|
|
12
|
+
* Falls back to the daemon's root only when there is no session to ask — the same fallback the
|
|
13
|
+
* journal and the run artifact use, and for the same reason: a picture of somebody's app has no
|
|
14
|
+
* business in a directory that has nothing to do with it.
|
|
15
|
+
*/
|
|
16
|
+
function captureRoot(deps, sessionId) {
|
|
17
|
+
try {
|
|
18
|
+
return deps.sessions.resolve(sessionId).artifactRoot ?? deps.reticleRoot;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return deps.reticleRoot;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
9
24
|
import { trackCaptureDirectory } from './capture-cleanup.js';
|
|
10
25
|
import { readFile, unlink } from 'node:fs/promises';
|
|
11
26
|
import { tmpdir } from 'node:os';
|
|
@@ -253,7 +268,16 @@ export const VISUAL_TOOLS = [
|
|
|
253
268
|
return reason === VisualReason.NO_PROVIDER ? noProvider : { ok: false, reason };
|
|
254
269
|
}
|
|
255
270
|
const name = asString(args['name']) ?? 'default';
|
|
256
|
-
|
|
271
|
+
// Beside the app that was captured. The tool RETURNS this path to the agent, so a wrong root
|
|
272
|
+
// is not only a stray PNG — it is a path the agent is told to look at in a tree that has
|
|
273
|
+
// nothing to do with the screenshot.
|
|
274
|
+
//
|
|
275
|
+
// Taken from the SESSION's own stamp rather than re-resolved, which is what `attach-journal`
|
|
276
|
+
// and the run artifact already do: the capture came from that session, so the answer is
|
|
277
|
+
// already in hand, and a second resolution is a second chance to disagree. It also keeps this
|
|
278
|
+
// directory from reaching into `project` for an address it was deliberately freed from
|
|
279
|
+
// needing — see `directory-reach`.
|
|
280
|
+
const store = new VisualStore(deps.fs, captureRoot(deps, sessionId));
|
|
257
281
|
// Scoped to the runtime that produced it: an Electron window, a Tauri webview and a browser
|
|
258
282
|
// tab do not render the same url the same way, and one shared baseline makes every
|
|
259
283
|
// cross-runtime diff wrong. See visualDir.
|
|
@@ -292,7 +316,7 @@ export const VISUAL_TOOLS = [
|
|
|
292
316
|
},
|
|
293
317
|
handler: async (deps, args) => {
|
|
294
318
|
const baseline = asString(args['baseline']) ?? '';
|
|
295
|
-
const store = new VisualStore(deps.fs, deps
|
|
319
|
+
const store = new VisualStore(deps.fs, captureRoot(deps, asString(args['sessionId'])));
|
|
296
320
|
// Capture FIRST, then fetch the baseline for the runtime that produced these pixels. Reading it
|
|
297
321
|
// by the session's runtime looked equivalent and is not: the route decides the renderer, so a
|
|
298
322
|
// desktop session captured through a driven browser must be compared against the WEB baseline,
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { writeHarnessSwitch } from './memory/cloud/harness-switch.js';
|
|
2
|
+
import { harnessConfigSource } from './memory/cloud/harness-config.js';
|
|
3
|
+
import { fetchPlatformConfig } from './features/harness/platform-config.js';
|
|
1
4
|
import { join } from 'node:path';
|
|
2
5
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
6
|
import { homedir } from 'node:os';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
7
|
+
import { linkedCloudPort } from './memory/cloud/cloud-config.js';
|
|
8
|
+
import { attachCloudSync } from './memory/cloud/sync-daemon.js';
|
|
6
9
|
import { wireHooks } from './hooks/hook-commands.js';
|
|
7
|
-
import { PROJECT_REGISTRY_FILE,
|
|
8
|
-
import { discoverProjectConfigs
|
|
9
|
-
import {
|
|
10
|
+
import { PROJECT_REGISTRY_FILE, parseProjectRegistry, projectCandidates, } from '@reticlehq/core/artifacts';
|
|
11
|
+
import { discoverProjectConfigs } from './command/cli/config/config-discovery.js';
|
|
12
|
+
import { artifactRootResolver } from './memory/project/artifact-root-resolver.js';
|
|
10
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
14
|
import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
|
|
12
15
|
import { originOf } from './portal/session/session-manager.js';
|
|
@@ -25,6 +28,7 @@ import { initImpact } from './memory/impact/impact-recorder.js';
|
|
|
25
28
|
import { FlowStore } from './language/flows/flows.js';
|
|
26
29
|
import { buildFlowChips } from './language/flows/flow-scope.js';
|
|
27
30
|
import { ProjectStore } from './memory/project/project-store.js';
|
|
31
|
+
import { projectStoreResolver } from './memory/project/project-for-root.js';
|
|
28
32
|
import { attachRouteLearning } from './memory/project/learned-routes.js';
|
|
29
33
|
import { AnnotationStore } from './language/flows/stores/annotation-store.js';
|
|
30
34
|
import { createNodeFileSystem } from './memory/project/fs/fs-port.js';
|
|
@@ -169,7 +173,15 @@ function attachJournal(bridge, deps) {
|
|
|
169
173
|
// Stamp the project's own `.reticle` before ANY counter fires for this session. Without it every
|
|
170
174
|
// verdict is recorded against wherever the daemon was started, which is how one app's evidence
|
|
171
175
|
// reached a different account's production dashboard.
|
|
172
|
-
|
|
176
|
+
// The origin is passed for the case where the page never stamped a project id: it is the only
|
|
177
|
+
// distinguishing fact left, and without it every such app shares one bucket.
|
|
178
|
+
session.artifactRoot = resolveArtifactRoot(session.projectId, originOf(session.url)).root;
|
|
179
|
+
// Here rather than on the start path, which was neither the moment we were about to write into a
|
|
180
|
+
// repository nor the root we were about to write into: it created `.reticle/` — holding nothing
|
|
181
|
+
// but the ignore file — wherever the daemon was launched, coming back every boot after the user
|
|
182
|
+
// deleted it, while the journals this ignore protects landed in another tree, uncovered.
|
|
183
|
+
if (deps.enabled)
|
|
184
|
+
void ensureWorkspaceGitignore(deps.fs, session.artifactRoot);
|
|
173
185
|
journalAttach(session);
|
|
174
186
|
// Seed the learned ambient map so a fresh session starts knowing which regions churn, instead of
|
|
175
187
|
// re-learning from zero. Best-effort + async: a late seed still helps, a failure is silent.
|
|
@@ -181,7 +193,12 @@ function attachJournal(bridge, deps) {
|
|
|
181
193
|
}
|
|
182
194
|
});
|
|
183
195
|
// Teardown: flush the journal tail to disk + persist what this session learned.
|
|
184
|
-
bridge.attachSessionEnd(makeSessionEnd(
|
|
196
|
+
bridge.attachSessionEnd(makeSessionEnd({
|
|
197
|
+
...deps,
|
|
198
|
+
// Retention runs from teardown, and it must not delete the journal of a session that is still
|
|
199
|
+
// being written. The registry is the only thing that knows which those are.
|
|
200
|
+
liveSessionIds: () => new Set(bridge.sessions.all().map((s) => s.id)),
|
|
201
|
+
}));
|
|
185
202
|
/*
|
|
186
203
|
* And the same write, DURING the session rather than only at the end of it.
|
|
187
204
|
*
|
|
@@ -201,16 +218,15 @@ function attachJournal(bridge, deps) {
|
|
|
201
218
|
});
|
|
202
219
|
}
|
|
203
220
|
if (deps.enabled) {
|
|
204
|
-
|
|
221
|
+
// Empty in the ordinary case (nothing has connected yet), but this path also runs on a daemon
|
|
222
|
+
// that is already serving sessions.
|
|
223
|
+
void pruneSessions(deps.fs, deps.reticleRoot, {
|
|
224
|
+
live: new Set(bridge.sessions.all().map((s) => s.id)),
|
|
225
|
+
});
|
|
205
226
|
// The largest thing in the workspace, and until now the only one with no delete path at all.
|
|
206
227
|
void pruneVisualDiffs(deps.fs, deps.reticleRoot);
|
|
207
228
|
// Write-only local copies of reports the outbox already carries.
|
|
208
229
|
void pruneFeedback(deps.fs, deps.reticleRoot);
|
|
209
|
-
// Here rather than in `init`, because this is the moment we are actually about to write into
|
|
210
|
-
// somebody's repository — and the paths that reach it without ever running `init` (a plugin
|
|
211
|
-
// install, a hand-added client config) are exactly the ones that would otherwise leave an
|
|
212
|
-
// unexplained pile of untracked files behind. Best-effort and write-once; see the helper.
|
|
213
|
-
void ensureWorkspaceGitignore(deps.fs, deps.reticleRoot);
|
|
214
230
|
}
|
|
215
231
|
}
|
|
216
232
|
/**
|
|
@@ -309,39 +325,18 @@ function knownProjectRoots() {
|
|
|
309
325
|
}
|
|
310
326
|
return [...roots];
|
|
311
327
|
}
|
|
312
|
-
function artifactRootResolver(daemonRoot) {
|
|
313
|
-
return (projectId) => {
|
|
314
|
-
let registry = emptyProjectRegistry();
|
|
315
|
-
try {
|
|
316
|
-
const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
|
|
317
|
-
registry = existsSync(path)
|
|
318
|
-
? parseProjectRegistry(JSON.parse(readFileSync(path, 'utf8')))
|
|
319
|
-
: registry;
|
|
320
|
-
}
|
|
321
|
-
catch {
|
|
322
|
-
// A cache that cannot be read is an empty cache, never an error: the daemon still resolves
|
|
323
|
-
// through discovery, and falls back to its own root exactly as it did before this existed.
|
|
324
|
-
}
|
|
325
|
-
let discovery = { found: [], searched: [] };
|
|
326
|
-
try {
|
|
327
|
-
discovery = discoverProjectConfigs(process.cwd());
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
// Same reasoning: a diagnostic search that throws must not take a tool call with it.
|
|
331
|
-
}
|
|
332
|
-
return resolveArtifactRoot({
|
|
333
|
-
projectId,
|
|
334
|
-
candidates: projectCandidatesFrom(discovery, registry),
|
|
335
|
-
daemonRoot,
|
|
336
|
-
});
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
328
|
export async function start(options = {}) {
|
|
340
329
|
const port = options.port ?? RETICLE_DEFAULT_PORT;
|
|
341
330
|
// Open the user's impact record before anything can connect. Not inside the MCP branch: a daemon
|
|
342
331
|
// serving a browser with no agent attached still has a HUD to answer, and a report that reads
|
|
343
332
|
// "nothing recorded yet" over a month of history on disk is the worst version of this feature.
|
|
344
|
-
initImpact({
|
|
333
|
+
initImpact({
|
|
334
|
+
reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT),
|
|
335
|
+
// The daemon owns both sides of this seam, so it is the layer that may join them: the cache
|
|
336
|
+
// lives in cloud memory, the platform read lives in the harness feature, and neither is allowed
|
|
337
|
+
// to reach for the other.
|
|
338
|
+
config: harnessConfigSource(() => fetchPlatformConfig(process.env)),
|
|
339
|
+
});
|
|
345
340
|
const uninstallHooks = wireHooks(options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT), readProjectId(process.cwd()));
|
|
346
341
|
const security = await resolveBridgeSecurityWithAutoToken(options);
|
|
347
342
|
const bridge = new Bridge({ port, sdkFix: sdkFixForCwd, ...security });
|
|
@@ -381,7 +376,7 @@ export async function start(options = {}) {
|
|
|
381
376
|
flows,
|
|
382
377
|
});
|
|
383
378
|
const project = new ProjectStore(fs, reticleRoot, { now });
|
|
384
|
-
attachRouteLearning(bridge, project);
|
|
379
|
+
attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
|
|
385
380
|
const annotations = new AnnotationStore();
|
|
386
381
|
pool = createBrowserPool(options.headless ?? true);
|
|
387
382
|
leaseReaper = new LeaseReaper(pool);
|
|
@@ -462,7 +457,13 @@ export async function startDaemon(options = {}) {
|
|
|
462
457
|
// was never opened in the process the HUD talks to: tool calls still recorded (the dispatch
|
|
463
458
|
// chokepoint opens it lazily), but a tab that connected before the first tool call was pushed
|
|
464
459
|
// nothing, so the report read "nothing recorded yet" over a file with history in it.
|
|
465
|
-
initImpact({
|
|
460
|
+
initImpact({
|
|
461
|
+
reticleRoot: options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT),
|
|
462
|
+
// The daemon owns both sides of this seam, so it is the layer that may join them: the cache
|
|
463
|
+
// lives in cloud memory, the platform read lives in the harness feature, and neither is allowed
|
|
464
|
+
// to reach for the other.
|
|
465
|
+
config: harnessConfigSource(() => fetchPlatformConfig(process.env)),
|
|
466
|
+
});
|
|
466
467
|
const security = await resolveBridgeSecurityWithAutoToken(options);
|
|
467
468
|
const shared = createSharedServer(security.token === undefined ? {} : { token: security.token });
|
|
468
469
|
const bridge = new Bridge({ port, server: shared.httpServer, sdkFix: sdkFixForCwd, ...security });
|
|
@@ -477,7 +478,7 @@ export async function startDaemon(options = {}) {
|
|
|
477
478
|
// answers WHY rather than just "sessionCount: 0".
|
|
478
479
|
// `verifyPort` rides along so a later `serve --http` can tell whether this daemon already honours
|
|
479
480
|
// the requested `--http-port` instead of silently ignoring the flag (#687).
|
|
480
|
-
shared.attachStatus(() => statusPayload(bridge.sessions.count(), bridge.sessions.list(), bridge.sessions.noSessionHint(), verifyHttp?.port));
|
|
481
|
+
shared.attachStatus(() => statusPayload(bridge.sessions.count(), bridge.sessions.list(), bridge.sessions.noSessionHint(), verifyHttp?.port, bridge.sessions.noSessionLead()));
|
|
481
482
|
// Agent-independent presence: the daemon outlives any single agent, so when the LAST agent's MCP
|
|
482
483
|
// connection drops (it stopped, or is waiting on the human), end every session and push a clear
|
|
483
484
|
// "go to your terminal" notice to the panel — the human is on the browser and must not lose a typed
|
|
@@ -504,7 +505,7 @@ export async function startDaemon(options = {}) {
|
|
|
504
505
|
/*
|
|
505
506
|
* Bound AFTER the sync daemon exists, read only when a run is actually written.
|
|
506
507
|
*
|
|
507
|
-
* `attachJournal` runs before `
|
|
508
|
+
* `attachJournal` runs before `attachCloudSync` because the journal has to be capturing before
|
|
508
509
|
* anything can connect, and reordering them so this could be a direct reference would put sync
|
|
509
510
|
* setup ahead of session capture for the sake of one callback.
|
|
510
511
|
*/
|
|
@@ -519,7 +520,7 @@ export async function startDaemon(options = {}) {
|
|
|
519
520
|
onRunPersisted: () => syncNudge.run?.(),
|
|
520
521
|
});
|
|
521
522
|
const project = new ProjectStore(fs, reticleRoot, { now });
|
|
522
|
-
attachRouteLearning(bridge, project);
|
|
523
|
+
attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
|
|
523
524
|
const annotations = new AnnotationStore();
|
|
524
525
|
const pool = createBrowserPool(options.headless ?? true);
|
|
525
526
|
const leaseReaper = new LeaseReaper(pool);
|
|
@@ -540,19 +541,22 @@ export async function startDaemon(options = {}) {
|
|
|
540
541
|
* events a user most wants: the ones at the start of a run.
|
|
541
542
|
*/
|
|
542
543
|
wireHooks(reticleRoot, readProjectId(process.cwd()));
|
|
543
|
-
const cloudSync =
|
|
544
|
+
const cloudSync = attachCloudSync({
|
|
545
|
+
fs,
|
|
544
546
|
reticleRoot,
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
// reporting nothing — indistinguishable, on the dashboard, from nobody having verified anything.
|
|
549
|
-
otherRoots: () => Promise.resolve(knownProjectRoots()),
|
|
550
|
-
cloudFor: (root) => resolveProjectCloud(fs, root, homedir(), process.env),
|
|
547
|
+
homeDir: homedir(),
|
|
548
|
+
env: process.env,
|
|
549
|
+
otherRoots: knownProjectRoots,
|
|
551
550
|
});
|
|
552
551
|
syncNudge.run = () => cloudSync.nudge();
|
|
553
|
-
//
|
|
554
|
-
//
|
|
552
|
+
// `syncNow`, not `nudge`: a nudge schedules a cycle soon, which is right for "a run landed" and
|
|
553
|
+
// wrong for a button somebody is watching. Never awaited.
|
|
555
554
|
bridge.attachSyncRequest(() => void cloudSync.syncNow());
|
|
555
|
+
// The panel's harness switch. Written through to the platform rather than kept locally, so the
|
|
556
|
+
// console and the panel cannot disagree about a setting they both offer. Nothing is awaited and a
|
|
557
|
+
// failure is not surfaced: the next snapshot re-reads the platform, so a lost write shows up as
|
|
558
|
+
// the switch springing back, which is the truthful outcome.
|
|
559
|
+
bridge.attachHarnessRequest((enabled) => void writeHarnessSwitch(process.env, enabled));
|
|
556
560
|
// Scope auto-selection to the active project (from .reticle.json) so a stray tab from another app is
|
|
557
561
|
// never picked when the agent omits a sessionId. Explicit per-call scope/sessionId still overrides.
|
|
558
562
|
// Scope + the no-session diagnosis: "no browser session connected" is the error that ends most
|
|
@@ -574,6 +578,7 @@ export async function startDaemon(options = {}) {
|
|
|
574
578
|
project,
|
|
575
579
|
fs,
|
|
576
580
|
reticleRoot,
|
|
581
|
+
linkedCloud: linkedCloudPort(fs, reticleRoot, homedir(), process.env),
|
|
577
582
|
// The long-lived daemon needs this MORE than the standalone MCP process does, not less: it is
|
|
578
583
|
// the one that outlives a single project and serves every app on the machine. Omitting it here
|
|
579
584
|
// silently disabled per-session artifact resolution for every agent that attaches to a running
|