@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
package/dist/status-payload.js
CHANGED
|
@@ -2,7 +2,9 @@ import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
|
|
|
2
2
|
import { SERVER_VERSION } from './command/version/identity/server-version.js';
|
|
3
3
|
export function statusPayload(sessionCount, sessions,
|
|
4
4
|
/** The no-session diagnosis, injected so this stays pure and the port probe stays off this path. */
|
|
5
|
-
why, verifyPort
|
|
5
|
+
why, verifyPort,
|
|
6
|
+
/** The short rendering of the same diagnosis. Absent on a daemon that has no provider wired. */
|
|
7
|
+
whyLead) {
|
|
6
8
|
return {
|
|
7
9
|
running: true,
|
|
8
10
|
version: SERVER_VERSION,
|
|
@@ -12,6 +14,7 @@ why, verifyPort) {
|
|
|
12
14
|
// Only when there is nothing to explain away: a diagnosis printed beside a live session would
|
|
13
15
|
// contradict it.
|
|
14
16
|
...(0 === sessionCount && why !== undefined ? { why } : {}),
|
|
17
|
+
...(0 === sessionCount && whyLead !== undefined ? { whyLead } : {}),
|
|
15
18
|
...(verifyPort === undefined ? {} : { verifyPort }),
|
|
16
19
|
};
|
|
17
20
|
}
|
|
@@ -4,7 +4,6 @@ import { MCP_SSE_PATH, MCP_MESSAGE_PATH, MCP_SHUTDOWN_EVENT, STATUS_PATH, DRIVE_
|
|
|
4
4
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
5
5
|
import { noteAgentPeer, PEER_VERSION_PARAM, PEER_CONTRACT_PARAM, } from '../command/version/peer-announce.js';
|
|
6
6
|
import { log } from '../log.js';
|
|
7
|
-
import { reportMcpConnected } from '../telemetry/mcp-connection.js';
|
|
8
7
|
import { isLoopbackHost, isLocalWebOrigin, isLoopbackPeer, requestToken, tokensMatch, } from '../portal/bridge/token-auth.js';
|
|
9
8
|
import { LOOPBACK_IDLE_MS } from './loopback-agent.js';
|
|
10
9
|
/**
|
|
@@ -177,10 +176,9 @@ export function createSharedServer(options = {}) {
|
|
|
177
176
|
.connect(transport)
|
|
178
177
|
.then(() => {
|
|
179
178
|
log('mcp_client_connected', { sessionId: sid });
|
|
180
|
-
// The
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
reportMcpConnected();
|
|
179
|
+
// The telemetry for this moved to `oninitialized`, where the client has said its name.
|
|
180
|
+
// An SSE stream that opens here and never handshakes is a probe, not a client, and
|
|
181
|
+
// reporting it from this point produced a row that could never name who attached.
|
|
184
182
|
})
|
|
185
183
|
.catch((err) => {
|
|
186
184
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* often does a real user's MCP server go down, and does it come back — had no answer outside the
|
|
7
7
|
* machine it happened on.
|
|
8
8
|
*
|
|
9
|
-
* Capped at
|
|
9
|
+
* Capped at one benign and one fault event PER STAGE per proxy process, and the cap is the design
|
|
10
|
+
* rather than a limitation. One
|
|
10
11
|
* measured afternoon produced 547 proxy reconnects; an event per reconnect would bill for the
|
|
11
12
|
* pathology instead of measuring it, which is the same mistake the per-call `tool` event made before
|
|
12
13
|
* it was removed. What a dashboard needs is the SHARE OF SESSIONS that lose MCP at all (the first
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* often does a real user's MCP server go down, and does it come back — had no answer outside the
|
|
7
7
|
* machine it happened on.
|
|
8
8
|
*
|
|
9
|
-
* Capped at
|
|
9
|
+
* Capped at one benign and one fault event PER STAGE per proxy process, and the cap is the design
|
|
10
|
+
* rather than a limitation. One
|
|
10
11
|
* measured afternoon produced 547 proxy reconnects; an event per reconnect would bill for the
|
|
11
12
|
* pathology instead of measuring it, which is the same mistake the per-call `tool` event made before
|
|
12
13
|
* it was removed. What a dashboard needs is the SHARE OF SESSIONS that lose MCP at all (the first
|
|
@@ -28,6 +29,19 @@ const KNOWN_REASONS = new Set(Object.values(OutageReason));
|
|
|
28
29
|
function outageReason(raw) {
|
|
29
30
|
return KNOWN_REASONS.has(raw) ? raw : OutageReason.OTHER;
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The daemon retiring on schedule is not a fault, and it is by far the commonest drop.
|
|
34
|
+
*
|
|
35
|
+
* Kept as the CLASS rather than the reason itself, because the cap is keyed on it: one benign drop
|
|
36
|
+
* and one fault per stage, not one event per reason. Keying on the reason would put the volume the
|
|
37
|
+
* cap exists to prevent straight back — a flapping proxy has several reasons available to it and
|
|
38
|
+
* would bill for each of them — while keying on the stage alone is what let a scheduled shutdown at
|
|
39
|
+
* minute one permanently suppress a real outage at minute forty, biasing the metric toward exactly
|
|
40
|
+
* the rows `daemon_shutdown` was introduced to exclude.
|
|
41
|
+
*/
|
|
42
|
+
function isBenign(reason) {
|
|
43
|
+
return OutageReason.DAEMON_SHUTDOWN === reason;
|
|
44
|
+
}
|
|
31
45
|
const reported = new Set();
|
|
32
46
|
/** Reset between tests; a real process reports each stage at most once. */
|
|
33
47
|
export function resetOutageReporting() {
|
|
@@ -39,13 +53,15 @@ export function resetOutageReporting() {
|
|
|
39
53
|
* fails — the reconnect it is describing.
|
|
40
54
|
*/
|
|
41
55
|
export function reportMcpOutage(stage, facts) {
|
|
42
|
-
|
|
56
|
+
const reason = outageReason(facts.reason);
|
|
57
|
+
const slot = `${stage}:${isBenign(reason) ? 'benign' : 'fault'}`;
|
|
58
|
+
if (reported.has(slot))
|
|
43
59
|
return;
|
|
44
|
-
reported.add(
|
|
60
|
+
reported.add(slot);
|
|
45
61
|
void getTelemetry().emit(TelemetryEventKind.MCP_CONNECTION_LOST, {
|
|
46
62
|
outage: {
|
|
47
63
|
stage,
|
|
48
|
-
reason
|
|
64
|
+
reason,
|
|
49
65
|
attempts: facts.attempts,
|
|
50
66
|
// Always sent, including zero: zero is the finding — a drop no agent could feel. See
|
|
51
67
|
// McpOutageSchema.pendingLost.
|
|
@@ -107,4 +107,13 @@ export declare function startMcpProxy(port: number,
|
|
|
107
107
|
*/
|
|
108
108
|
ensureDaemon?: () => Promise<void>,
|
|
109
109
|
/** Observe the real shutdown boundary in transport tests without terminating their worker. */
|
|
110
|
-
exitProcess?: (code: number) => void
|
|
110
|
+
exitProcess?: (code: number) => void,
|
|
111
|
+
/**
|
|
112
|
+
* Tell the resilience layer this session proved itself usable.
|
|
113
|
+
*
|
|
114
|
+
* The same `endpoint` frame that earns a fresh retry budget below also clears the disconnect-storm
|
|
115
|
+
* counter, and for the same reason: response headers are produced by a daemon that accepts SSE and
|
|
116
|
+
* drops it, and an `endpoint` frame is not. Optional, because a transport test drives this
|
|
117
|
+
* function with no resilience installed.
|
|
118
|
+
*/
|
|
119
|
+
onUsableSession?: () => void): Promise<never>;
|
|
@@ -22,7 +22,7 @@ import { flushProxySessionMetrics } from '../../telemetry/proxy-telemetry.js';
|
|
|
22
22
|
* the whole defect this import closes was the proxy answering a DIFFERENT question from every other
|
|
23
23
|
* surface, and a second copy of the probe is how that comes back.
|
|
24
24
|
*/
|
|
25
|
-
import { fetchStatus as fetchDaemonStatus } from '../../command/
|
|
25
|
+
import { fetchStatus as fetchDaemonStatus } from '../../command/daemon/binding/daemon-status-probe.js';
|
|
26
26
|
/**
|
|
27
27
|
* Re-exported so every existing caller (and every spec) keeps importing the proxy's log from the
|
|
28
28
|
* proxy. Where the code lives is a file-size decision; where it is imported from is an API.
|
|
@@ -304,7 +304,16 @@ export function startMcpProxy(port,
|
|
|
304
304
|
*/
|
|
305
305
|
ensureDaemon,
|
|
306
306
|
/** Observe the real shutdown boundary in transport tests without terminating their worker. */
|
|
307
|
-
exitProcess = (code) => process.exit(code)
|
|
307
|
+
exitProcess = (code) => process.exit(code),
|
|
308
|
+
/**
|
|
309
|
+
* Tell the resilience layer this session proved itself usable.
|
|
310
|
+
*
|
|
311
|
+
* The same `endpoint` frame that earns a fresh retry budget below also clears the disconnect-storm
|
|
312
|
+
* counter, and for the same reason: response headers are produced by a daemon that accepts SSE and
|
|
313
|
+
* drops it, and an `endpoint` frame is not. Optional, because a transport test drives this
|
|
314
|
+
* function with no resilience installed.
|
|
315
|
+
*/
|
|
316
|
+
onUsableSession = () => undefined) {
|
|
308
317
|
return new Promise((_resolve, reject) => {
|
|
309
318
|
// A client just started us, which is the only honest evidence that Reticle is registered with
|
|
310
319
|
// one at all: a config entry proves somebody wrote JSON, not that a client ever read it. Paired
|
|
@@ -452,13 +461,11 @@ exitProcess = (code) => process.exit(code)) {
|
|
|
452
461
|
const msg = parseJsonRpc(line);
|
|
453
462
|
if (null === msg || msg.id === undefined || !pending.take(msg.id))
|
|
454
463
|
return;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
});
|
|
461
|
-
}
|
|
464
|
+
// No outage is reported here, deliberately. A POST leg that died while the SSE stream is
|
|
465
|
+
// still up is not the agent losing its tools: `postSocketFailures` on the session summary
|
|
466
|
+
// already counts it, and reporting it as a lost stream both double-counted and — because
|
|
467
|
+
// the outage cap is per process — let a transient POST failure in the first minute suppress
|
|
468
|
+
// the real stream outage in the fortieth.
|
|
462
469
|
proxyLog('reticle_mcp_proxy_post_unanswered', {
|
|
463
470
|
port,
|
|
464
471
|
method: String(msg.method),
|
|
@@ -490,6 +497,10 @@ exitProcess = (code) => process.exit(code)) {
|
|
|
490
497
|
reportMcpOutage(OutageStage.RECOVERED, { reason: lastDropReason, attempts: cost });
|
|
491
498
|
attempts = 0;
|
|
492
499
|
attemptsBeforeDormant = 0;
|
|
500
|
+
// The disconnects behind us were reconnects, not a runaway. Counting them for the life of
|
|
501
|
+
// the process made twenty daemon restarts over a working week indistinguishable from twenty
|
|
502
|
+
// in eight seconds, and the proxy left on the former.
|
|
503
|
+
onUsableSession();
|
|
493
504
|
// The new session's McpServer has never seen the client's initialize — replay it first, then
|
|
494
505
|
// flush whatever the client sent while we were reconnecting.
|
|
495
506
|
for (const line of replay.replayLines())
|
|
@@ -12,7 +12,16 @@ export declare const CALL_TOOL_METHOD = "tools/call";
|
|
|
12
12
|
* Non-session-bound tools (and tools with no outputSchema) are returned unchanged.
|
|
13
13
|
*/
|
|
14
14
|
export declare function withSessionEnvelope(name: string, outputSchema: z.ZodRawShape | undefined): z.ZodRawShape | undefined;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Abbreviations whose internal period is NOT a sentence end.
|
|
17
|
+
*
|
|
18
|
+
* The splitter looked for the first `". "`, which "e.g. " satisfies — so every lean-profile
|
|
19
|
+
* description containing one was cut off mid-abbreviation. `reticle_act`'s ref parameter reached the
|
|
20
|
+
* agent as "Element ref from reticle_snapshot or reticle_query (e.g." and stopped: the example gone,
|
|
21
|
+
* and any contract stated after it gone too. Silent, and it degraded the DEFAULT profile only, which
|
|
22
|
+
* is the one nobody reads the raw strings for.
|
|
23
|
+
*/
|
|
24
|
+
export { firstSentence } from '../tools/first-sentence.js';
|
|
16
25
|
/**
|
|
17
26
|
* Serialize a tool result to the MCP `text` content block. The agent consumes this text — the
|
|
18
27
|
* typed contract travels separately as `structuredContent`, unchanged by encoding — so the text
|
package/dist/surface/mcp/mcp.js
CHANGED
|
@@ -36,6 +36,7 @@ import { log } from '../../log.js';
|
|
|
36
36
|
import { SERVER_VERSION } from '../../command/version/identity/server-version.js';
|
|
37
37
|
import { setMcpClientNameHook } from '../../telemetry/feedback-context.js';
|
|
38
38
|
import { getSessionMetrics } from '../../telemetry/session-metrics.js';
|
|
39
|
+
import { reportMcpConnected } from '../../telemetry/mcp-connection.js';
|
|
39
40
|
import { parsePredicate } from '@reticlehq/engine/question/predicate/predicate-parse.js';
|
|
40
41
|
/**
|
|
41
42
|
* Merge the runtime-spliced envelope (health/lease/age/control) into a session-bound tool's declared
|
|
@@ -70,16 +71,9 @@ function withExample(description, example) {
|
|
|
70
71
|
* and any contract stated after it gone too. Silent, and it degraded the DEFAULT profile only, which
|
|
71
72
|
* is the one nobody reads the raw strings for.
|
|
72
73
|
*/
|
|
73
|
-
|
|
74
|
-
export
|
|
75
|
-
|
|
76
|
-
const base = nl >= 0 ? description.slice(0, nl) : description;
|
|
77
|
-
// Mask abbreviations with an equal-length filler so offsets stay valid in the original string.
|
|
78
|
-
const masked = ABBREVIATIONS.reduce((text, abbr) => text.split(abbr).join('\u0000'.repeat(abbr.length)), base);
|
|
79
|
-
const dot = masked.search(/\.\s/);
|
|
80
|
-
const sentence = dot >= 0 ? base.slice(0, dot + 1) : base;
|
|
81
|
-
return sentence.length > 160 ? `${sentence.slice(0, 159)}…` : sentence;
|
|
82
|
-
}
|
|
74
|
+
// Re-exported because callers (and `mcp.test.ts`) have imported it from here since it lived here.
|
|
75
|
+
export { firstSentence } from '../tools/first-sentence.js';
|
|
76
|
+
import { firstSentence } from '../tools/first-sentence.js';
|
|
83
77
|
/**
|
|
84
78
|
* Parameters carrying the recursive predicate grammar. Advertised compactly in lean profiles.
|
|
85
79
|
*
|
|
@@ -124,8 +118,8 @@ const PREDICATE_KINDS = 'Predicate object: { kind, ...fields }. kind is one of e
|
|
|
124
118
|
* paying for six.
|
|
125
119
|
*/
|
|
126
120
|
const PREDICATE_FIELD_GRAMMAR_HINT = ' Bug-catching options: net.count (exact request count — catches double-submit), ' +
|
|
127
|
-
'net.
|
|
128
|
-
'
|
|
121
|
+
'net.bodyMatches (a shallow JSON match on what the SERVER answered — say the FIELD and its value, ' +
|
|
122
|
+
'because a substring of the body matches key names too), net.requestBodyMatches (the same on what ' +
|
|
129
123
|
'the UI SENT — the verdict for "applying this filter actually puts it in the payload"), ' +
|
|
130
124
|
'console.absent:true (action completed with a CLEAN ' +
|
|
131
125
|
'console), absent:true on element/text (it should be gone). Call reticle_tools for the full field ' +
|
|
@@ -218,21 +212,33 @@ tools = tableForSurface(surface)) {
|
|
|
218
212
|
const shipped = new Set(tableForSurface(surface).map((tool) => tool.name));
|
|
219
213
|
const ours = tools.filter((tool) => shipped.has(tool.name));
|
|
220
214
|
const theirs = tools.filter((tool) => !shipped.has(tool.name));
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
215
|
+
/*
|
|
216
|
+
* `merged` used to drop `reticle_run`, on the reasoning that the hatch "exists to DISPATCH to a
|
|
217
|
+
* tool the surface does not advertise, and this surface advertises everything, so it has no job
|
|
218
|
+
* left".
|
|
219
|
+
*
|
|
220
|
+
* THE PREMISE WAS FALSE, and it had been false for some time. `MERGED_TOOL_NAMES` is
|
|
221
|
+
* `CORE_TOOL_NAMES` minus the merged-away names plus `LOOK`; `EXTENDED_TOOL_NAMES` — screenshot,
|
|
222
|
+
* visual diff, clock, network mock, storage, record, the flow pair, intent, context, capabilities
|
|
223
|
+
* — is not in it. So eleven registered tools were advertised by nothing, callable by nothing, and
|
|
224
|
+
* catalogued by nothing: `reticle_tools { names: ["reticle_screenshot"] }` answered `unknown
|
|
225
|
+
* tool`, on a build where the tool exists and is registered.
|
|
226
|
+
*
|
|
227
|
+
* The extended set's own comments say each demoted tool is "still one `reticle_run` hop from any
|
|
228
|
+
* agent that wants it". That is the trade the demotion was justified by, and dropping the hatch
|
|
229
|
+
* silently cancelled the half of it we owed the caller. One of the casualties was
|
|
230
|
+
* `reticle_visual_diff`, which covers the only bug class the registry marks as one Reticle cannot
|
|
231
|
+
* otherwise see.
|
|
232
|
+
*
|
|
233
|
+
* So the hatch stays, and the catalogue lists everything the hatch can reach. `reticle_tools`
|
|
234
|
+
* keeps its second job either way: every input schema on a trimmed surface is LEAN, and
|
|
235
|
+
* `{ names: [...] }` is the only way to get full parameters — which matters most here, because a
|
|
236
|
+
* merged tool's schema is the union of its members' fields with all of them optional. Recovery
|
|
237
|
+
* messages across this server also say "Call reticle_tools", and a surface without it turns that
|
|
238
|
+
* advice into a dead end (see surface-sizes.test.ts).
|
|
239
|
+
*/
|
|
231
240
|
const advertisedHere = filterTools([...ours], surface);
|
|
232
|
-
const
|
|
233
|
-
? new Set([...advertisedHere, ...theirs].map((tool) => tool.name))
|
|
234
|
-
: undefined;
|
|
235
|
-
const meta = buildDynamicTools([...tools], origin, callable).filter((tool) => surface !== TOOL_SURFACE.MERGED || tool.name !== ReticleTool.RUN);
|
|
241
|
+
const meta = buildDynamicTools([...tools], origin, undefined);
|
|
236
242
|
return [...advertisedHere, ...theirs, ...meta];
|
|
237
243
|
}
|
|
238
244
|
/**
|
|
@@ -512,8 +518,14 @@ tools = tableForSurface(profile)) {
|
|
|
512
518
|
*/
|
|
513
519
|
server.server.oninitialized = () => {
|
|
514
520
|
const info = server.server.getClientVersion();
|
|
515
|
-
if (info?.name
|
|
516
|
-
|
|
521
|
+
if (info?.name === undefined)
|
|
522
|
+
return;
|
|
523
|
+
getSessionMetrics().recordClient(info.name, info.version);
|
|
524
|
+
// The one signal that separates "Reticle is running" from "somebody is USING it", reported from
|
|
525
|
+
// the first moment it can carry WHO. It used to fire from the SSE `connect()` resolution, which
|
|
526
|
+
// happens before the handshake — so every connect row was anonymous, and the field meant to say
|
|
527
|
+
// which agent client converts best was empty on every event ever sent.
|
|
528
|
+
reportMcpConnected(info.name);
|
|
517
529
|
};
|
|
518
530
|
// Which agent is on the other end, for a feedback report. Registered as a lazy hook rather than
|
|
519
531
|
// read here: the handshake has not happened yet at construction time, and a report filed twenty
|
|
@@ -540,7 +552,25 @@ tools = tableForSurface(profile)) {
|
|
|
540
552
|
const advertised = advertisedTools(profile, tools);
|
|
541
553
|
// The names this surface actually hands the agent. Every piece of advice leaving this server is
|
|
542
554
|
// rewritten against it, so a static string cannot route a reader to a tool they were not given.
|
|
543
|
-
|
|
555
|
+
/**
|
|
556
|
+
* What a name in advice can actually REACH, which is not the same as what is advertised.
|
|
557
|
+
*
|
|
558
|
+
* `liveCallText` rewrites mentions of tools this surface cannot reach — it exists because advice
|
|
559
|
+
* naming `reticle_flow_save` on a surface without it sent agents at a tool that answered "not
|
|
560
|
+
* reachable on this tool surface". Gated on the ADVERTISED set, it kept firing once the dispatch
|
|
561
|
+
* hatch came back: `reticle_flow_save` and `reticle_record` are reachable through
|
|
562
|
+
* `reticle_run` but not advertised, so every mention of them — INCLUDING their own names and the
|
|
563
|
+
* parameter docs that name the recording to save — was rewritten to
|
|
564
|
+
* `reticle_verify { action: "explore", persona }`. Two tools with the same wrong name, and a
|
|
565
|
+
* `flowName` description telling the reader to pass the name that a tool which is not the one
|
|
566
|
+
* they called was called with.
|
|
567
|
+
*
|
|
568
|
+
* With a hatch, every registered tool is reachable and no rewriting is owed. Without one,
|
|
569
|
+
* advertised IS reachable, which is the case this was written for.
|
|
570
|
+
*/
|
|
571
|
+
const advertisedNames = advertised.some((tool) => ReticleTool.RUN === tool.name)
|
|
572
|
+
? new Set(tools.map((tool) => tool.name))
|
|
573
|
+
: new Set(advertised.map((tool) => tool.name));
|
|
544
574
|
installFriendlyArgErrors(server, new Map(advertised
|
|
545
575
|
.filter((tool) => tool.example !== undefined)
|
|
546
576
|
.map((tool) => [tool.name, JSON.stringify(tool.example)])),
|
|
@@ -5,5 +5,18 @@ export declare function daemonPollDelayMs(attempt: number): number;
|
|
|
5
5
|
* inside the daemon just to check reachability.
|
|
6
6
|
*/
|
|
7
7
|
export declare function probeDaemon(port: number): Promise<boolean>;
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Poll until a Reticle daemon is SERVING on `port`, or the deadline passes.
|
|
10
|
+
*
|
|
11
|
+
* "Accepts a TCP connection" is not the question. A daemon wedged mid-start accepts and never
|
|
12
|
+
* serves, and so does a stranger holding the port — `port-presence.ts` calls both FOREIGN for
|
|
13
|
+
* exactly this reason, and every other decision point in the codebase was migrated onto it. This
|
|
14
|
+
* one was not, so the wake path spawned a daemon, waited on a bare connect, and dialled a corpse.
|
|
15
|
+
*
|
|
16
|
+
* Probes are injected (defaulting to the real pair) so the rule is testable without a socket, which
|
|
17
|
+
* is the same shape `probePresence`'s own callers use.
|
|
18
|
+
*/
|
|
19
|
+
export declare function waitForDaemon(port: number, probes?: {
|
|
20
|
+
tcpOpen: (port: number) => Promise<boolean>;
|
|
21
|
+
status: (port: number) => Promise<unknown>;
|
|
22
|
+
}): Promise<void>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as net from 'node:net';
|
|
2
2
|
import { LOOPBACK_HOST } from '@reticlehq/core';
|
|
3
|
+
import { describePresence, presenceIsUsable, probePresence, PortPresence, } from '../../../command/daemon/binding/port-presence.js';
|
|
4
|
+
import { fetchStatus } from '../../../command/daemon/binding/daemon-status-probe.js';
|
|
3
5
|
const DEFAULT_DAEMON_READY_TIMEOUT_MS = 10_000;
|
|
4
6
|
/**
|
|
5
7
|
* How long to wait for the spawned daemon's port to accept connections before giving up. The default
|
|
@@ -39,16 +41,31 @@ export function probeDaemon(port) {
|
|
|
39
41
|
socket.connect(port, LOOPBACK_HOST);
|
|
40
42
|
});
|
|
41
43
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Poll until a Reticle daemon is SERVING on `port`, or the deadline passes.
|
|
46
|
+
*
|
|
47
|
+
* "Accepts a TCP connection" is not the question. A daemon wedged mid-start accepts and never
|
|
48
|
+
* serves, and so does a stranger holding the port — `port-presence.ts` calls both FOREIGN for
|
|
49
|
+
* exactly this reason, and every other decision point in the codebase was migrated onto it. This
|
|
50
|
+
* one was not, so the wake path spawned a daemon, waited on a bare connect, and dialled a corpse.
|
|
51
|
+
*
|
|
52
|
+
* Probes are injected (defaulting to the real pair) so the rule is testable without a socket, which
|
|
53
|
+
* is the same shape `probePresence`'s own callers use.
|
|
54
|
+
*/
|
|
55
|
+
export async function waitForDaemon(port, probes = { tcpOpen: probeDaemon, status: fetchStatus }) {
|
|
44
56
|
const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
|
|
45
57
|
let attempt = 0;
|
|
58
|
+
let presence = PortPresence.FREE;
|
|
46
59
|
while (Date.now() < deadline) {
|
|
47
|
-
|
|
48
|
-
if (
|
|
60
|
+
presence = await probePresence(port, probes);
|
|
61
|
+
if (presenceIsUsable(presence))
|
|
49
62
|
return;
|
|
50
63
|
attempt++;
|
|
51
64
|
await delay(daemonPollDelayMs(attempt));
|
|
52
65
|
}
|
|
53
|
-
|
|
66
|
+
// The state we timed out in is the whole diagnosis — FREE means the daemon never came up, FOREIGN
|
|
67
|
+
// means something is squatting the port. Reporting only the timeout sent readers hunting the
|
|
68
|
+
// wrong one.
|
|
69
|
+
throw new Error(`reticle daemon did not become ready on port ${String(port)} within ` +
|
|
70
|
+
`${String(DAEMON_READY_TIMEOUT_MS)}ms — ${describePresence(presence, port)}`);
|
|
54
71
|
}
|
|
@@ -27,18 +27,12 @@ export declare function accountProxyLogWrite(bytesSinceCheck: number, written: n
|
|
|
27
27
|
/**
|
|
28
28
|
* Reclaim a proxy log that a previous process already let run away. Returns the bytes reclaimed.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* the cap alone does not help them: nobody should have to find one with `du`.
|
|
30
|
+
* Path-shaped wrapper over `recoverOversizedLog`, which is where the rule, the in-place truncation
|
|
31
|
+
* and the ENOSPC incident behind them are written down. Both of Reticle's logs are capped by that
|
|
32
|
+
* one function; this keeps the name and the `fileSize`/`truncateFile` deps its callers already pass.
|
|
34
33
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* until that handle closes — which is why `: > ~/.reticle/proxy-4400.log` is the operation to copy
|
|
38
|
-
* here.
|
|
39
|
-
*
|
|
40
|
-
* Best-effort, in the same spirit as `rotateDaemonLog`: refusing to start the MCP server over
|
|
41
|
-
* housekeeping is strictly worse than a large file.
|
|
34
|
+
* Such files already exist on machines that ran an older build, and the cap alone does not help
|
|
35
|
+
* them: nobody should have to find one with `du`.
|
|
42
36
|
*/
|
|
43
37
|
export declare function recoverOversizedProxyLog(path: string, deps: {
|
|
44
38
|
fileSize(p: string): number;
|
|
@@ -10,7 +10,7 @@ import { homedir } from 'node:os';
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { ReticleDir } from '@reticlehq/core';
|
|
12
12
|
import { log } from '../../../log.js';
|
|
13
|
-
import { MAX_DAEMON_LOG_BYTES, rotateDaemonLog } from '../../../command/daemon/daemon.js';
|
|
13
|
+
import { MAX_DAEMON_LOG_BYTES, recoverOversizedLog, rotateDaemonLog, } from '../../../command/daemon/daemon.js';
|
|
14
14
|
/**
|
|
15
15
|
* Which port this proxy serves, for the log file name. Set once at startup.
|
|
16
16
|
*
|
|
@@ -46,30 +46,15 @@ export function accountProxyLogWrite(bytesSinceCheck, written, path, deps) {
|
|
|
46
46
|
/**
|
|
47
47
|
* Reclaim a proxy log that a previous process already let run away. Returns the bytes reclaimed.
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* the cap alone does not help them: nobody should have to find one with `du`.
|
|
49
|
+
* Path-shaped wrapper over `recoverOversizedLog`, which is where the rule, the in-place truncation
|
|
50
|
+
* and the ENOSPC incident behind them are written down. Both of Reticle's logs are capped by that
|
|
51
|
+
* one function; this keeps the name and the `fileSize`/`truncateFile` deps its callers already pass.
|
|
53
52
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* until that handle closes — which is why `: > ~/.reticle/proxy-4400.log` is the operation to copy
|
|
57
|
-
* here.
|
|
58
|
-
*
|
|
59
|
-
* Best-effort, in the same spirit as `rotateDaemonLog`: refusing to start the MCP server over
|
|
60
|
-
* housekeeping is strictly worse than a large file.
|
|
53
|
+
* Such files already exist on machines that ran an older build, and the cap alone does not help
|
|
54
|
+
* them: nobody should have to find one with `du`.
|
|
61
55
|
*/
|
|
62
56
|
export function recoverOversizedProxyLog(path, deps, max = MAX_DAEMON_LOG_BYTES) {
|
|
63
|
-
|
|
64
|
-
const size = deps.fileSize(path);
|
|
65
|
-
if (max >= size)
|
|
66
|
-
return 0;
|
|
67
|
-
deps.truncateFile(path);
|
|
68
|
-
return size;
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return 0;
|
|
72
|
-
}
|
|
57
|
+
return recoverOversizedLog({ size: () => deps.fileSize(path), truncate: () => deps.truncateFile(path) }, max);
|
|
73
58
|
}
|
|
74
59
|
/** The real filesystem behind the two rotation helpers. */
|
|
75
60
|
const proxyLogFileOps = {
|
|
@@ -14,7 +14,7 @@ import { PredicateSchema } from '@reticlehq/engine/question/predicate/predicate-
|
|
|
14
14
|
import { isGlobalPressCall } from '@reticlehq/core';
|
|
15
15
|
import { SessionReplacedError } from '../../../portal/session/page-commands/pending-commands.js';
|
|
16
16
|
import { assertNativeInputSupported } from './act-danger.js';
|
|
17
|
-
import { unevaluablePredicateReason } from '@reticlehq/engine/question/predicate/predicate-precheck.js';
|
|
17
|
+
import { unevaluablePredicateReason, vacuousPredicateReason, } from '@reticlehq/engine/question/predicate/predicate-precheck.js';
|
|
18
18
|
/**
|
|
19
19
|
* Refuse a sequence whose steps cannot be addressed.
|
|
20
20
|
*
|
|
@@ -137,6 +137,11 @@ export function preflightAct(actArgs, until) {
|
|
|
137
137
|
const unevaluable = unevaluablePredicateReason(until);
|
|
138
138
|
if (unevaluable !== undefined)
|
|
139
139
|
throw new Error(unevaluable);
|
|
140
|
+
// A predicate that was already true when it was written answered `no-fault` / `already_true` —
|
|
141
|
+
// verdict-shaped, and carrying no proof of anything the action did. See vacuousPredicateReason.
|
|
142
|
+
const vacuous = vacuousPredicateReason(until);
|
|
143
|
+
if (vacuous !== undefined)
|
|
144
|
+
throw new Error(vacuous);
|
|
140
145
|
}
|
|
141
146
|
/**
|
|
142
147
|
* Dispatch a write, or report that its outcome went unobserved.
|
|
@@ -39,6 +39,19 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
|
|
|
39
39
|
because: `a declared consequence did not hold — observed ${observed}`,
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
+
/*
|
|
43
|
+
* `unknown` rather than `no-fault`, and the distinction is load-bearing.
|
|
44
|
+
*
|
|
45
|
+
* "Nothing was declared" is what `no-fault` means for a single action -- but the engine only says
|
|
46
|
+
* it over a SETTLED window, because no-fault claims the whole window was observed. Nothing on
|
|
47
|
+
* this path observes settledness: the caller tracks dispatch and stalls, never settle. Saying
|
|
48
|
+
* `no-fault` here would claim evidence nobody collected, in the one place this product cannot
|
|
49
|
+
* afford to overclaim.
|
|
50
|
+
*
|
|
51
|
+
* So the enum stays conservative and the SENTENCE carries the whole answer: it names declaring a
|
|
52
|
+
* consequence as the next move, which is what an agent reading `no-fault` would have done.
|
|
53
|
+
* Upgrading means plumbing `settled` from the act results through `SequencePlan`.
|
|
54
|
+
*/
|
|
42
55
|
if (0 === declared) {
|
|
43
56
|
return {
|
|
44
57
|
verified: Verified.UNKNOWN,
|
|
@@ -27,7 +27,12 @@ export async function saveFailedAssertCapsule(inputs) {
|
|
|
27
27
|
// Same rule and same resolver as every other artifact: a capsule written into a sibling repo is
|
|
28
28
|
// evidence filed against a codebase that did not produce it, and it outlives the turn.
|
|
29
29
|
// The caller's answer wins; the resolver is the fallback for callers that have no session.
|
|
30
|
-
const root = given ??
|
|
30
|
+
const root = given ?? bestEffortRoot(deps, asString(args['sessionId']));
|
|
31
|
+
// No directory this capsule could honestly belong to, so it is not filed. The two things this
|
|
32
|
+
// must not do are write it somewhere arbitrary and throw: the assertion has already failed, the
|
|
33
|
+
// verdict is already built, and losing the capsule must never also lose the verdict.
|
|
34
|
+
if (root === undefined)
|
|
35
|
+
return undefined;
|
|
31
36
|
const saved = await new CapsuleStore(deps.fs, root).save({
|
|
32
37
|
version: CAPSULE_VERSION,
|
|
33
38
|
id,
|
|
@@ -51,3 +56,24 @@ export async function saveFailedAssertCapsule(inputs) {
|
|
|
51
56
|
});
|
|
52
57
|
return saved ? id : undefined;
|
|
53
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Where this capsule goes, or nothing — never a throw and never somebody else's checkout.
|
|
61
|
+
*
|
|
62
|
+
* `sessionRoot` refuses a `sessionId` that names no session, because for a tool the agent CALLED
|
|
63
|
+
* the alternative is silently writing into the daemon's own project. Here the caller is not a tool
|
|
64
|
+
* the agent called: it is the evidence capture hanging off a verdict that has already been decided,
|
|
65
|
+
* and on this path a refusal would convert a red assertion into a tool error. Both of the answers
|
|
66
|
+
* that refusal exists to prevent are still prevented — nothing is written, and nothing is written
|
|
67
|
+
* to the wrong root — by declining to file the capsule at all.
|
|
68
|
+
*
|
|
69
|
+
* Reachable only when the act's own session AND its navigation successor are gone by the time the
|
|
70
|
+
* verdict lands, since `act_and_wait` passes the root of the session it actually drove.
|
|
71
|
+
*/
|
|
72
|
+
function bestEffortRoot(deps, sessionId) {
|
|
73
|
+
try {
|
|
74
|
+
return sessionRoot(deps, sessionId);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|