@reticlehq/server 2.10.0 → 2.12.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 +73 -11
- package/dist/bridge/bridge.d.ts +0 -13
- package/dist/bridge/bridge.js +1 -1
- package/dist/bridge/bridge.test-harness.d.ts +2 -0
- package/dist/bridge/bridge.test-harness.js +8 -1
- package/dist/capsule/capsule-store.d.ts +2 -1
- package/dist/capsule/capsule-store.js +1 -1
- package/dist/capsule/causal-summary.d.ts +2 -2
- package/dist/chromium-launch-options.d.ts +28 -0
- package/dist/chromium-launch-options.js +29 -0
- package/dist/cli/cli-doctor.js +8 -0
- package/dist/cli/cli-flow-commands.d.ts +3 -1
- package/dist/cli/cli-flow-commands.js +34 -4
- package/dist/cli/cli-kill.d.ts +2 -1
- package/dist/cli/cli-parse.d.ts +3 -1
- package/dist/cli/cli-parse.js +29 -7
- package/dist/cli/cli-update-commands.js +20 -1
- package/dist/cli/cli-verify.d.ts +1 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/cloud-cli.js +12 -6
- package/dist/cli/config-discovery.d.ts +2 -1
- package/dist/cli/doctor-rows.d.ts +3 -1
- package/dist/cli/doctor-rows.js +3 -1
- package/dist/cli/gate-exit.d.ts +21 -0
- package/dist/cli/gate-exit.js +21 -0
- package/dist/cli/gate-hook-message.d.ts +33 -0
- package/dist/cli/gate-hook-message.js +46 -0
- package/dist/cli/mcp-command.d.ts +25 -0
- package/dist/cli/mcp-command.js +143 -0
- package/dist/cli/sibling-ports.d.ts +37 -0
- package/dist/cli/sibling-ports.js +56 -0
- package/dist/cli.js +37 -110
- package/dist/cloud/cloud-config.d.ts +2 -1
- package/dist/cloud/cloud-sync.d.ts +11 -0
- package/dist/cloud/cloud-sync.js +32 -0
- package/dist/crawl/crawl.d.ts +9 -13
- package/dist/crawl/crawl.js +20 -3
- package/dist/daemon/daemon-resolve.d.ts +79 -0
- package/dist/daemon/daemon-resolve.js +118 -0
- package/dist/daemon/free-port.d.ts +29 -0
- package/dist/daemon/free-port.js +55 -0
- package/dist/daemon/heartbeat.d.ts +0 -4
- package/dist/daemon/heartbeat.js +2 -2
- package/dist/dev/stale-issue-guard.d.ts +41 -0
- package/dist/dev/stale-issue-guard.js +75 -0
- package/dist/domain/domain-tools.js +3 -2
- package/dist/events/contradictions.d.ts +41 -0
- package/dist/events/contradictions.js +125 -12
- package/dist/events/declared.d.ts +14 -0
- package/dist/events/declared.js +33 -0
- package/dist/events/echo-mismatch.js +14 -0
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +17 -0
- package/dist/events/predicate-asks.d.ts +30 -0
- package/dist/events/predicate-asks.js +48 -0
- package/dist/events/predicate-eval.js +254 -25
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +51 -5
- package/dist/events/predicate-schema.d.ts +62 -0
- package/dist/events/predicate-schema.js +162 -4
- package/dist/events/predicate.d.ts +15 -0
- package/dist/events/predicate.js +41 -2
- package/dist/flows/coverage.d.ts +0 -8
- package/dist/flows/coverage.js +1 -1
- package/dist/flows/decision.d.ts +5 -1
- package/dist/flows/decision.js +33 -7
- package/dist/flows/flake.d.ts +0 -2
- package/dist/flows/flake.js +1 -1
- package/dist/flows/flow-intent.d.ts +52 -0
- package/dist/flows/flow-intent.js +123 -0
- package/dist/flows/flow-replay-run.js +27 -4
- package/dist/flows/flow-success.js +15 -3
- package/dist/flows/flow-tools.js +65 -8
- package/dist/flows/flows.d.ts +6 -1
- package/dist/flows/flows.js +34 -22
- package/dist/flows/predicate-to-expect.js +14 -3
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +9 -2
- package/dist/honesty/blind-spots.d.ts +8 -16
- package/dist/honesty/blind-spots.js +18 -5
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/gap-ledger.d.ts +52 -0
- package/dist/honesty/gap-ledger.js +63 -0
- package/dist/honesty/instrumentation-gaps.d.ts +79 -0
- package/dist/honesty/instrumentation-gaps.js +77 -0
- package/dist/honesty/observability.d.ts +37 -0
- package/dist/honesty/observability.js +40 -0
- package/dist/honesty/tool-hit-rate.d.ts +86 -0
- package/dist/honesty/tool-hit-rate.js +90 -0
- package/dist/honesty/undeclared-change.d.ts +51 -0
- package/dist/honesty/undeclared-change.js +56 -0
- package/dist/honesty/verified.d.ts +15 -0
- package/dist/honesty/verified.js +68 -4
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +1 -1
- package/dist/hunt/hunt-report.d.ts +0 -2
- package/dist/hunt/hunt-report.js +1 -1
- package/dist/impact/impact-store.d.ts +0 -5
- package/dist/impact/impact-store.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +41 -0
- package/dist/init/agent-rules.d.ts +17 -0
- package/dist/init/agent-rules.js +31 -1
- package/dist/init/capabilities.d.ts +0 -2
- package/dist/init/capabilities.js +1 -1
- package/dist/init/closing-hint.d.ts +36 -0
- package/dist/init/closing-hint.js +120 -0
- package/dist/init/confirm.d.ts +0 -25
- package/dist/init/confirm.js +3 -3
- package/dist/init/cra.d.ts +14 -1
- package/dist/init/cra.js +43 -12
- package/dist/init/dev-script.d.ts +48 -0
- package/dist/init/dev-script.js +63 -0
- package/dist/init/mcp.d.ts +16 -5
- package/dist/init/mcp.js +26 -10
- package/dist/init/node-io.js +3 -0
- package/dist/init/plan-framework.d.ts +0 -5
- package/dist/init/plan-framework.js +83 -9
- package/dist/init/plan.d.ts +10 -30
- package/dist/init/plan.js +30 -15
- package/dist/init/refresh-rules.d.ts +39 -0
- package/dist/init/refresh-rules.js +51 -0
- package/dist/init/run.d.ts +2 -10
- package/dist/init/run.js +26 -68
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +33 -10
- package/dist/init/snippets.js +53 -10
- package/dist/input/real-input.d.ts +2 -0
- package/dist/input/real-input.js +3 -2
- package/dist/input/scroll-find.d.ts +7 -4
- package/dist/input/scroll-find.js +63 -25
- package/dist/intent/inline-intent.d.ts +46 -0
- package/dist/intent/inline-intent.js +103 -0
- package/dist/intent/intent-store.d.ts +37 -0
- package/dist/intent/intent-store.js +112 -0
- package/dist/intent/intent-tools.d.ts +2 -0
- package/dist/intent/intent-tools.js +80 -0
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +25 -0
- package/dist/journal/envelope.d.ts +2 -2
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/license/license.d.ts +8 -2
- package/dist/license/license.js +10 -15
- package/dist/mcp/attach-memory.d.ts +67 -0
- package/dist/mcp/attach-memory.js +142 -0
- package/dist/mcp/mcp-proxy.d.ts +0 -7
- package/dist/mcp/mcp-proxy.js +14 -2
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +45 -23
- package/dist/mcp/server-instructions.js +45 -3
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +10 -0
- package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
- package/dist/oracles/flow-instrument-gaps.js +4 -5
- package/dist/oracles/self-instrument.d.ts +9 -9
- package/dist/oracles/self-instrument.js +19 -13
- package/dist/pool/browser-pool.d.ts +12 -1
- package/dist/pool/browser-pool.js +31 -1
- package/dist/pool/playwright-launcher.js +3 -1
- package/dist/project/artifact-root.d.ts +83 -0
- package/dist/project/artifact-root.js +112 -0
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.d.ts +9 -0
- package/dist/project/memory-fs.js +93 -0
- package/dist/project/project-store.d.ts +17 -0
- package/dist/project/project-store.js +33 -0
- package/dist/project/project-tools.js +3 -32
- package/dist/project/remember-project.d.ts +24 -0
- package/dist/project/remember-project.js +40 -0
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/project/session-root.d.ts +15 -0
- package/dist/project/session-root.js +31 -0
- package/dist/runs/context-tools.d.ts +2 -0
- package/dist/runs/context-tools.js +89 -0
- package/dist/runs/run-context.d.ts +40 -0
- package/dist/runs/run-context.js +158 -0
- package/dist/runs/run-diff.d.ts +2 -1
- package/dist/runs/run-tools.js +2 -1
- package/dist/runs/verification-sync.js +2 -2
- package/dist/session/dev-server-probe.d.ts +0 -8
- package/dist/session/dev-server-probe.js +1 -1
- package/dist/session/last-act.d.ts +7 -1
- package/dist/session/last-act.js +2 -2
- package/dist/session/lease-availability.d.ts +40 -0
- package/dist/session/lease-availability.js +43 -0
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +20 -0
- package/dist/session/no-session-diagnosis.js +41 -12
- package/dist/session/no-session-next-action.d.ts +5 -0
- package/dist/session/no-session-next-action.js +35 -3
- package/dist/session/no-session-watch.d.ts +8 -0
- package/dist/session/no-session-watch.js +83 -65
- package/dist/session/output-budget.js +4 -1
- package/dist/session/session-health.d.ts +9 -0
- package/dist/session/session-health.js +23 -0
- package/dist/session/session-manager.js +50 -1
- package/dist/session/session-successor.d.ts +37 -0
- package/dist/session/session-successor.js +78 -0
- package/dist/session/session.d.ts +40 -1
- package/dist/session/session.js +84 -2
- package/dist/session/stall-clock.d.ts +18 -0
- package/dist/session/stall-clock.js +48 -0
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/daemon-telemetry.js +12 -23
- package/dist/telemetry/feedback-outbox.d.ts +2 -1
- package/dist/telemetry/project-profile.d.ts +0 -8
- package/dist/telemetry/project-profile.js +1 -1
- package/dist/telemetry/run-telemetry.d.ts +0 -2
- package/dist/telemetry/run-telemetry.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +1 -1
- package/dist/telemetry/telemetry.d.ts +12 -3
- package/dist/telemetry/telemetry.js +25 -5
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-tools.js +159 -24
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +21 -2
- package/dist/tools/assert-verdict.js +59 -4
- package/dist/tools/browser-tools.js +0 -20
- package/dist/tools/complete-snapshot.d.ts +48 -0
- package/dist/tools/complete-snapshot.js +75 -0
- package/dist/tools/contract-tools.js +5 -3
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +124 -37
- package/dist/tools/dynamic-tools.js +29 -8
- package/dist/tools/error-recovery.d.ts +24 -14
- package/dist/tools/error-recovery.js +70 -9
- package/dist/tools/feedback-tools.d.ts +1 -1
- package/dist/tools/feedback-tools.js +2 -2
- package/dist/tools/invoke-tool.js +62 -2
- package/dist/tools/lease-hint.d.ts +10 -0
- package/dist/tools/lease-hint.js +53 -4
- package/dist/tools/lease-tools.d.ts +5 -1
- package/dist/tools/lease-tools.js +22 -8
- package/dist/tools/navigate-arrival.d.ts +0 -2
- package/dist/tools/navigate-arrival.js +1 -1
- package/dist/tools/observe-tools.js +70 -27
- package/dist/tools/read-tools.js +5 -3
- package/dist/tools/real-input-attempt.d.ts +22 -0
- package/dist/tools/real-input-attempt.js +153 -1
- package/dist/tools/reconcile-tools.js +18 -4
- package/dist/tools/resolve-target.d.ts +0 -7
- package/dist/tools/sdk-marker-probe.d.ts +29 -0
- package/dist/tools/sdk-marker-probe.js +57 -0
- package/dist/tools/snapshot-delta.d.ts +1 -1
- package/dist/tools/tool-kit.d.ts +28 -1
- package/dist/tools/tool-kit.js +16 -0
- package/dist/tools/tool-names.d.ts +3 -0
- package/dist/tools/tool-names.js +3 -0
- package/dist/tools/tool-surface.d.ts +85 -6
- package/dist/tools/tool-surface.js +122 -2
- package/dist/tools/tools.js +22 -4
- package/dist/tools/unadvertised-help.js +4 -4
- package/dist/tools/verify-next-baton.d.ts +53 -0
- package/dist/tools/verify-next-baton.js +52 -0
- package/dist/update/update-nudge.d.ts +7 -10
- package/dist/update/update-nudge.js +12 -3
- package/dist/update/updater.d.ts +1 -1
- package/docs/README.md +3 -1
- package/docs/cli/cloud.mdx +1 -1
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/verify.mdx +4 -5
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +3 -3
- package/docs/enterprise.md +3 -1
- package/docs/gates.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +145 -0
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages.mdx +8 -6
- package/docs/platform-integration.md +17 -0
- package/docs/predicates.mdx +8 -0
- package/docs/reticle-manual.md +106 -0
- package/docs/telemetry-contract.md +1 -12
- package/docs/telemetry-events.mdx +1 -34
- package/docs/telemetry.md +0 -1
- package/docs/tools-tools-and-run.mdx +2 -4
- package/docs/troubleshooting.mdx +9 -0
- package/docs/usage.md +3 -2
- package/package.json +2 -2
- package/dist/session/session-readiness.d.ts +0 -34
- package/dist/session/session-readiness.js +0 -36
- package/dist/telemetry/instrumentation-stall.d.ts +0 -57
- package/dist/telemetry/instrumentation-stall.js +0 -103
|
@@ -18,6 +18,14 @@ interface NoSessionWatchOptions {
|
|
|
18
18
|
/** Where that was decided — this daemon's working directory unless a caller says otherwise. */
|
|
19
19
|
directory?: string;
|
|
20
20
|
probe?: () => Promise<number[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Well-known Reticle ports other than ours that currently accept a connection.
|
|
23
|
+
*
|
|
24
|
+
* Remaining half of #261. Injected so tests can pin the observation without opening a socket.
|
|
25
|
+
* Production (no `probe` override) probes those ports itself. A test that injects `probe` and
|
|
26
|
+
* omits this is opting out: it owns the scan.
|
|
27
|
+
*/
|
|
28
|
+
occupiedSiblings?: () => Promise<readonly number[]>;
|
|
21
29
|
/**
|
|
22
30
|
* How many pooled leases have aged out, if a pool exists. Injected as a reader rather than the
|
|
23
31
|
* pool itself: the diagnosis needs one number, and taking the whole pool would tie the session
|
|
@@ -17,7 +17,9 @@ import { readProjectFramework, readProjectId, readProjectPort } from '../cli/cli
|
|
|
17
17
|
import { discoverProjectConfigs } from '../cli/config-discovery.js';
|
|
18
18
|
import { hasProjectConnectedBefore, rememberConnected } from './connection-memory.js';
|
|
19
19
|
import { reticleStateHome } from '../daemon/daemon.js';
|
|
20
|
-
import { stallUptime } from '
|
|
20
|
+
import { stallUptime } from './stall-clock.js';
|
|
21
|
+
import { probeDaemon } from '../mcp/mcp-proxy.js';
|
|
22
|
+
import { findOccupiedSiblings } from '../cli/sibling-ports.js';
|
|
21
23
|
/** Slow enough to be free, fast enough that a dev server started 15s ago is already reflected. */
|
|
22
24
|
const REFRESH_MS = 15_000;
|
|
23
25
|
/** The URL an auto-attach opens. `localhost` for the same reason the probe uses it: both stacks. */
|
|
@@ -56,6 +58,7 @@ export function startNoSessionWatch(options) {
|
|
|
56
58
|
* `options.probe` returns only the serving set, so this stays empty for injected probes.
|
|
57
59
|
*/
|
|
58
60
|
let slowListeners = [];
|
|
61
|
+
let siblingListeners = [];
|
|
59
62
|
let running = false;
|
|
60
63
|
/** Ports auto-attach has already spent its one attempt on. Bounded: never a loop, never a retry. */
|
|
61
64
|
const attempted = new Set();
|
|
@@ -66,6 +69,27 @@ export function startNoSessionWatch(options) {
|
|
|
66
69
|
// is routinely written by `init` AFTER this daemon started, so re-read rather than cache. See the
|
|
67
70
|
// `initialized` comment below, which this shares.
|
|
68
71
|
const isWired = () => options.initialized || readProjectId(directory) !== undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the daemon's current project scope once for both halves of a sessions response.
|
|
74
|
+
*
|
|
75
|
+
* This remains a live read because `init` commonly writes the config after the daemon starts.
|
|
76
|
+
*/
|
|
77
|
+
const projectScopeFacts = () => {
|
|
78
|
+
const initialized = isWired();
|
|
79
|
+
if (initialized)
|
|
80
|
+
return { initialized };
|
|
81
|
+
const discovery = discoverProjectConfigs(directory);
|
|
82
|
+
const elsewhere = discovery.found.filter((config) => config.directory !== directory);
|
|
83
|
+
return 0 === elsewhere.length
|
|
84
|
+
? { initialized, searchedDirectories: discovery.searched }
|
|
85
|
+
: {
|
|
86
|
+
initialized,
|
|
87
|
+
configsElsewhere: elsewhere.map((config) => ({
|
|
88
|
+
directory: config.directory,
|
|
89
|
+
...(config.projectId === undefined ? {} : { projectId: config.projectId }),
|
|
90
|
+
})),
|
|
91
|
+
};
|
|
92
|
+
};
|
|
69
93
|
// Read at boot: the daemon's own identity does not change under it, and this is the key the
|
|
70
94
|
// durable bit is stored under.
|
|
71
95
|
const stateDir = options.stateDir ?? reticleStateHome();
|
|
@@ -129,6 +153,12 @@ export function startNoSessionWatch(options) {
|
|
|
129
153
|
: probe())
|
|
130
154
|
.then(async (ports) => {
|
|
131
155
|
listening = ports;
|
|
156
|
+
siblingListeners =
|
|
157
|
+
options.occupiedSiblings !== undefined
|
|
158
|
+
? await options.occupiedSiblings()
|
|
159
|
+
: options.probe === undefined
|
|
160
|
+
? await findOccupiedSiblings(options.port, probeDaemon)
|
|
161
|
+
: [];
|
|
132
162
|
await autoAttach(ports);
|
|
133
163
|
})
|
|
134
164
|
.catch(() => {
|
|
@@ -141,77 +171,65 @@ export function startNoSessionWatch(options) {
|
|
|
141
171
|
refresh();
|
|
142
172
|
const timer = setInterval(refresh, REFRESH_MS);
|
|
143
173
|
timer.unref();
|
|
144
|
-
const nextAction = () => nextActionFor({
|
|
174
|
+
const nextAction = (scope) => nextActionFor({
|
|
145
175
|
everConnected: options.sessions.everConnected(),
|
|
146
|
-
initialized:
|
|
176
|
+
initialized: scope.initialized,
|
|
177
|
+
...(scope.configsElsewhere === undefined ? {} : { configsElsewhere: scope.configsElsewhere }),
|
|
147
178
|
previouslyConnected: connectedBefore(),
|
|
148
179
|
listening,
|
|
149
180
|
// Read when asked, like everything else here: a `package.json` can gain a dev script, and a
|
|
150
181
|
// daemon that cached "there is none" at boot would keep saying so for the rest of the day.
|
|
151
182
|
dev: detectDevCommand(directory),
|
|
152
183
|
});
|
|
153
|
-
options.sessions.setNoSessionNextAction(nextAction);
|
|
154
|
-
options.sessions.setNoSessionHint(() =>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
})(),
|
|
203
|
-
// Read here rather than at boot: `.reticle.json` can be written by `init` after this daemon
|
|
204
|
-
// started, which is the ordinary first-install order, and a port cached from before it existed
|
|
205
|
-
// would make the daemon confidently report no mismatch on the one run where there is one.
|
|
206
|
-
...(() => {
|
|
207
|
-
const configured = readProjectPort(directory);
|
|
208
|
-
return configured === undefined ? {} : { projectPort: configured };
|
|
209
|
-
})(),
|
|
210
|
-
}) +
|
|
211
|
-
// Prose for the human, then the literal command for the agent. Appended rather than folded
|
|
212
|
-
// into the diagnosis so that file stays pure and its cases stay independently pinned.
|
|
213
|
-
` ${renderNextAction(nextAction())}` +
|
|
214
|
-
(attachFailure ?? ''));
|
|
184
|
+
options.sessions.setNoSessionNextAction(() => nextAction(projectScopeFacts()));
|
|
185
|
+
options.sessions.setNoSessionHint(() => {
|
|
186
|
+
const scope = projectScopeFacts();
|
|
187
|
+
return (diagnoseNoSession({
|
|
188
|
+
everConnected: options.sessions.everConnected(),
|
|
189
|
+
// Read WHEN ASKED, for the same reason `projectPort` below is: `.reticle.json` is routinely
|
|
190
|
+
// written by `init` after this daemon started — that is the ordinary first-install order — and
|
|
191
|
+
// the boot-time answer is then permanently stale. Reported from the field as `reticle status`
|
|
192
|
+
// saying the project had never been through `init` about a project whose config named its
|
|
193
|
+
// framework and its projectId, and whose real problem was a dev server older than the plugin.
|
|
194
|
+
// The boot value still counts: it is the one the daemon scoped its sessions with.
|
|
195
|
+
...scope,
|
|
196
|
+
listening,
|
|
197
|
+
slowListeners,
|
|
198
|
+
port: options.port,
|
|
199
|
+
// The directory `initialized` was decided in. Named in the message because "there is no
|
|
200
|
+
// `.reticle.json`" is a claim about ONE directory, and a reader standing somewhere else
|
|
201
|
+
// cannot tell whether it is a claim about their app at all.
|
|
202
|
+
directory,
|
|
203
|
+
// The one fact that outranks every absence below it, and the reason a fresh daemon stopped
|
|
204
|
+
// claiming that an install which has demonstrably worked has never worked.
|
|
205
|
+
previouslyConnected: connectedBefore(),
|
|
206
|
+
// Ranks the causes. Read when asked, like the rest: `init` writes this file after the daemon
|
|
207
|
+
// starts on an ordinary first install.
|
|
208
|
+
...(() => {
|
|
209
|
+
const framework = readProjectFramework(directory);
|
|
210
|
+
return framework === undefined ? {} : { framework };
|
|
211
|
+
})(),
|
|
212
|
+
leaseExpired: (options.reapedLeases?.() ?? 0) > 0,
|
|
213
|
+
// How long this daemon has been waiting with no app. The diagnosis uses it to surface
|
|
214
|
+
// "install never finished" — the same condition telemetry already knows about.
|
|
215
|
+
...(() => {
|
|
216
|
+
const upMs = stallUptime(Date.now());
|
|
217
|
+
return upMs === undefined ? {} : { daemonUpMs: upMs };
|
|
218
|
+
})(),
|
|
219
|
+
// Read here rather than at boot: `.reticle.json` can be written by `init` after this daemon
|
|
220
|
+
// started, which is the ordinary first-install order, and a port cached from before it existed
|
|
221
|
+
// would make the daemon confidently report no mismatch on the one run where there is one.
|
|
222
|
+
...(() => {
|
|
223
|
+
const configured = readProjectPort(directory);
|
|
224
|
+
return configured === undefined ? {} : { projectPort: configured };
|
|
225
|
+
})(),
|
|
226
|
+
...(0 === siblingListeners.length ? {} : { siblingListeners }),
|
|
227
|
+
}) +
|
|
228
|
+
// Prose for the human, then the literal command for the agent. Both consume the same scope
|
|
229
|
+
// facts so a discovered workspace config cannot become an `init` recommendation below it.
|
|
230
|
+
` ${renderNextAction(nextAction(scope))}` +
|
|
231
|
+
(attachFailure ?? ''));
|
|
232
|
+
});
|
|
215
233
|
return () => {
|
|
216
234
|
clearInterval(timer);
|
|
217
235
|
options.sessions.setConnectionRecorder(undefined);
|
|
@@ -50,7 +50,10 @@ export function estimateTokens(text) {
|
|
|
50
50
|
}
|
|
51
51
|
export function sizeCost(payload) {
|
|
52
52
|
const json = JSON.stringify(payload) ?? '';
|
|
53
|
-
|
|
53
|
+
// Actual UTF-8 wire bytes, not json.length (UTF-16 code units) — the field is labeled `bytes`
|
|
54
|
+
// and drives the large-snapshot recommendation, so a multibyte-heavy payload was under-counted.
|
|
55
|
+
const bytes = Buffer.byteLength(json, 'utf8');
|
|
56
|
+
return { bytes, tokens: estimateTokens(json) };
|
|
54
57
|
}
|
|
55
58
|
/**
|
|
56
59
|
* Attach a `cost` size preview to a read result. Pure: the cost is computed over the result BEFORE
|
|
@@ -45,6 +45,15 @@ export declare function healthEnvelope(session: Session): HealthEnvelope;
|
|
|
45
45
|
* warn-only so background testing never breaks.
|
|
46
46
|
*/
|
|
47
47
|
export declare function refuseIfThrottled(session: Session, refuse: unknown): void;
|
|
48
|
+
/**
|
|
49
|
+
* Suffix the starvation note onto a FAILED predicate verdict when the tab is throttled. A pass is
|
|
50
|
+
* returned untouched — starvation is context for a failure, not a reason to doubt a hold. Pure and
|
|
51
|
+
* generic over the verdict shape so assert/wait_for/act_and_wait share one rule.
|
|
52
|
+
*/
|
|
53
|
+
export declare function annotateStarvedFailure<V extends {
|
|
54
|
+
pass?: boolean;
|
|
55
|
+
failureReason?: string;
|
|
56
|
+
}>(session: Session, verdict: V): V;
|
|
48
57
|
/** The page's own visibility/runtime report, narrowed out of an untrusted PAGE_HEALTH payload. */
|
|
49
58
|
interface HealthReport {
|
|
50
59
|
hidden: boolean | undefined;
|
|
@@ -35,6 +35,29 @@ export function refuseIfThrottled(session, refuse) {
|
|
|
35
35
|
throw new Error(`refusing to act: ${THROTTLED_WARNING}`);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The sentence appended to a FAILED predicate verdict waited out on a throttled tab.
|
|
40
|
+
*
|
|
41
|
+
* A starved tab and a missing element read identically as a bare near-miss: after a hard reload a
|
|
42
|
+
* backgrounded tab can sit on its loading state forever (hydration starved, nothing painted), and
|
|
43
|
+
* `wait_for { text }` timing out there looks exactly like "the code did not render" (#521). The
|
|
44
|
+
* health envelope already rode alongside saying `throttled:true`, but nothing connected it to the
|
|
45
|
+
* verdict an agent actually gates on. Appended, not replacing: the original diagnosis still leads,
|
|
46
|
+
* and on a healthy tab the failure says exactly what it always said.
|
|
47
|
+
*/
|
|
48
|
+
const STARVED_WAIT_NOTE = ' (the tab was throttled during this wait, so starvation can look exactly like absence: bring it to the front and re-check before concluding this is genuinely missing)';
|
|
49
|
+
/**
|
|
50
|
+
* Suffix the starvation note onto a FAILED predicate verdict when the tab is throttled. A pass is
|
|
51
|
+
* returned untouched — starvation is context for a failure, not a reason to doubt a hold. Pure and
|
|
52
|
+
* generic over the verdict shape so assert/wait_for/act_and_wait share one rule.
|
|
53
|
+
*/
|
|
54
|
+
export function annotateStarvedFailure(session, verdict) {
|
|
55
|
+
if (true === verdict.pass || verdict.failureReason === undefined)
|
|
56
|
+
return verdict;
|
|
57
|
+
if (true !== session.throttled())
|
|
58
|
+
return verdict;
|
|
59
|
+
return { ...verdict, failureReason: `${verdict.failureReason}${STARVED_WAIT_NOTE}` };
|
|
60
|
+
}
|
|
38
61
|
/**
|
|
39
62
|
* Narrow a PAGE_HEALTH payload. Every field is optional on the wire — an older SDK does not report
|
|
40
63
|
* `runtime` at all — so each is returned as undefined rather than defaulted here, letting the caller
|
|
@@ -2,6 +2,7 @@ import { NO_SESSION_CONNECTED_ERROR } from '@reticlehq/core';
|
|
|
2
2
|
import { declareDrivenRedactionKeys, forgetDrivenRedactionKeys, } from '../input/driven-redaction.js';
|
|
3
3
|
import { Session } from './session.js';
|
|
4
4
|
import { AttachmentHistory } from './attachment-history.js';
|
|
5
|
+
import { pickDocumentSuccessor } from './session-successor.js';
|
|
5
6
|
/** The scheme://host:port of a URL, or undefined if it can't be parsed. Used to compare origins. */
|
|
6
7
|
export function originOf(url) {
|
|
7
8
|
if (url === undefined)
|
|
@@ -61,6 +62,14 @@ function scopeMissError(connected, scope) {
|
|
|
61
62
|
*/
|
|
62
63
|
/** How many bridge-initiated closes to remember for diagnosis. */
|
|
63
64
|
const MAX_REMEMBERED_CLOSURES = 5;
|
|
65
|
+
/**
|
|
66
|
+
* How many recently-departed session ids to remember for successor rebind.
|
|
67
|
+
*
|
|
68
|
+
* A full-document navigation HELLO's back under a new id while the agent still holds the old one.
|
|
69
|
+
* The tombstone is how `resolve(oldId)` finds the unique same-origin successor instead of refusing.
|
|
70
|
+
* Bounded so a long-lived daemon cannot accumulate one record per tab forever.
|
|
71
|
+
*/
|
|
72
|
+
const MAX_TOMBSTONES = 32;
|
|
64
73
|
export class SessionManager {
|
|
65
74
|
#sessions = new Map();
|
|
66
75
|
/**
|
|
@@ -69,6 +78,13 @@ export class SessionManager {
|
|
|
69
78
|
* Owned here because this is the one place that sees both halves — every add and every remove.
|
|
70
79
|
*/
|
|
71
80
|
#attachment = new AttachmentHistory();
|
|
81
|
+
/**
|
|
82
|
+
* Recently departed sessions, keyed by the id the agent still holds.
|
|
83
|
+
*
|
|
84
|
+
* Insertion order is LRU: the oldest is dropped when the cap is hit. A reconnect under the same
|
|
85
|
+
* id forgets its tombstone — the live session is the answer then.
|
|
86
|
+
*/
|
|
87
|
+
#tombstones = new Map();
|
|
72
88
|
/**
|
|
73
89
|
* The active project's scope, set once from the daemon's .reticle.json. When a tool resolves a session
|
|
74
90
|
* without passing its own scope, this is applied — so auto-selection is project-scoped by default
|
|
@@ -100,6 +116,7 @@ export class SessionManager {
|
|
|
100
116
|
// that silently fails to register is a leak nothing would report.
|
|
101
117
|
declareDrivenRedactionKeys(session.id, session.redactKeys);
|
|
102
118
|
this.#attachment.attached(session.id);
|
|
119
|
+
this.#tombstones.delete(session.id);
|
|
103
120
|
return previous;
|
|
104
121
|
}
|
|
105
122
|
remove(session) {
|
|
@@ -110,8 +127,40 @@ export class SessionManager {
|
|
|
110
127
|
// Recorded, not forgotten: a session that comes back needs its gap measured, and a listing after
|
|
111
128
|
// the reconnect is exactly where that matters.
|
|
112
129
|
this.#attachment.detached(session.id);
|
|
130
|
+
this.#rememberTombstone(session);
|
|
113
131
|
return this.#sessions.delete(session.id);
|
|
114
132
|
}
|
|
133
|
+
#rememberTombstone(session) {
|
|
134
|
+
const origin = originOf(session.url);
|
|
135
|
+
if (origin === undefined)
|
|
136
|
+
return;
|
|
137
|
+
if (this.#tombstones.has(session.id))
|
|
138
|
+
this.#tombstones.delete(session.id);
|
|
139
|
+
this.#tombstones.set(session.id, {
|
|
140
|
+
id: session.id,
|
|
141
|
+
url: session.url,
|
|
142
|
+
...(session.projectId === undefined ? {} : { projectId: session.projectId }),
|
|
143
|
+
});
|
|
144
|
+
while (this.#tombstones.size > MAX_TOMBSTONES) {
|
|
145
|
+
const oldest = this.#tombstones.keys().next().value;
|
|
146
|
+
if (oldest === undefined)
|
|
147
|
+
break;
|
|
148
|
+
this.#tombstones.delete(oldest);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
#successorOf(sessionId) {
|
|
152
|
+
const departed = this.#tombstones.get(sessionId);
|
|
153
|
+
if (departed === undefined)
|
|
154
|
+
return undefined;
|
|
155
|
+
const picked = pickDocumentSuccessor(this.all().map((s) => ({
|
|
156
|
+
id: s.id,
|
|
157
|
+
url: s.url,
|
|
158
|
+
...(s.projectId === undefined ? {} : { projectId: s.projectId }),
|
|
159
|
+
})), departed);
|
|
160
|
+
if (picked === undefined)
|
|
161
|
+
return undefined;
|
|
162
|
+
return this.#sessions.get(picked.id);
|
|
163
|
+
}
|
|
115
164
|
get(sessionId) {
|
|
116
165
|
return this.#sessions.get(sessionId);
|
|
117
166
|
}
|
|
@@ -239,7 +288,7 @@ export class SessionManager {
|
|
|
239
288
|
}
|
|
240
289
|
resolve(sessionId, scope) {
|
|
241
290
|
if (sessionId !== undefined) {
|
|
242
|
-
const found = this.#sessions.get(sessionId);
|
|
291
|
+
const found = this.#sessions.get(sessionId) ?? this.#successorOf(sessionId);
|
|
243
292
|
if (found === undefined) {
|
|
244
293
|
throw new Error(this.#unknownSessionError(sessionId));
|
|
245
294
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session that took over after a full-document navigation.
|
|
3
|
+
*
|
|
4
|
+
* Reload keeps the id (sessionStorage). A click that loads a new document often does not: the SDK
|
|
5
|
+
* is torn down with the page and HELLO's back under a new id, while the agent still holds the old
|
|
6
|
+
* one. `navigate` already waits for that HELLO (`awaitArrival`) and returns the new id.
|
|
7
|
+
* `act_and_wait` and `assert` did not, so an MPA drive ended at the first link.
|
|
8
|
+
*
|
|
9
|
+
* Unique at the departed origin (and project, when known). Two live tabs at that origin is still a
|
|
10
|
+
* guess, and a guess here would drive the wrong app.
|
|
11
|
+
*/
|
|
12
|
+
import type { Session } from './session.js';
|
|
13
|
+
export interface SessionIdentity {
|
|
14
|
+
id: string;
|
|
15
|
+
url: string;
|
|
16
|
+
projectId?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SuccessorClock {
|
|
19
|
+
now: () => number;
|
|
20
|
+
sleep: (ms: number) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The one live session that can honestly take over from `departed`, or undefined when none or more
|
|
24
|
+
* than one could.
|
|
25
|
+
*
|
|
26
|
+
* A same-id reconnect is a successor: the id survived in sessionStorage, the document did not.
|
|
27
|
+
*/
|
|
28
|
+
export declare function pickDocumentSuccessor(live: readonly SessionIdentity[], departed: SessionIdentity): SessionIdentity | undefined;
|
|
29
|
+
export interface SuccessorRegistry {
|
|
30
|
+
get(id: string): Session | undefined;
|
|
31
|
+
all(): readonly Session[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Poll until a successor of `departed` is live, or the budget runs out. Null on timeout — never
|
|
35
|
+
* throws, because failing to follow is a legitimate `observation_lost`, not an error.
|
|
36
|
+
*/
|
|
37
|
+
export declare function awaitDocumentSuccessor(sessions: SuccessorRegistry, departed: Session, timeoutMs: number, clock?: SuccessorClock): Promise<Session | null>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session that took over after a full-document navigation.
|
|
3
|
+
*
|
|
4
|
+
* Reload keeps the id (sessionStorage). A click that loads a new document often does not: the SDK
|
|
5
|
+
* is torn down with the page and HELLO's back under a new id, while the agent still holds the old
|
|
6
|
+
* one. `navigate` already waits for that HELLO (`awaitArrival`) and returns the new id.
|
|
7
|
+
* `act_and_wait` and `assert` did not, so an MPA drive ended at the first link.
|
|
8
|
+
*
|
|
9
|
+
* Unique at the departed origin (and project, when known). Two live tabs at that origin is still a
|
|
10
|
+
* guess, and a guess here would drive the wrong app.
|
|
11
|
+
*/
|
|
12
|
+
const POLL_MS = 25;
|
|
13
|
+
const REAL_CLOCK = {
|
|
14
|
+
now: () => Date.now(),
|
|
15
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
16
|
+
};
|
|
17
|
+
function originOf(url) {
|
|
18
|
+
try {
|
|
19
|
+
return new URL(url).origin;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function identityOf(session) {
|
|
26
|
+
return {
|
|
27
|
+
id: session.id,
|
|
28
|
+
url: session.url,
|
|
29
|
+
...(session.projectId === undefined ? {} : { projectId: session.projectId }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The one live session that can honestly take over from `departed`, or undefined when none or more
|
|
34
|
+
* than one could.
|
|
35
|
+
*
|
|
36
|
+
* A same-id reconnect is a successor: the id survived in sessionStorage, the document did not.
|
|
37
|
+
*/
|
|
38
|
+
export function pickDocumentSuccessor(live, departed) {
|
|
39
|
+
const origin = originOf(departed.url);
|
|
40
|
+
if (origin === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
const atOrigin = live.filter((s) => originOf(s.url) === origin);
|
|
43
|
+
const sameId = atOrigin.find((s) => s.id === departed.id);
|
|
44
|
+
if (sameId !== undefined)
|
|
45
|
+
return sameId;
|
|
46
|
+
const others = atOrigin.filter((s) => s.id !== departed.id &&
|
|
47
|
+
(departed.projectId === undefined || s.projectId === departed.projectId));
|
|
48
|
+
if (1 !== others.length)
|
|
49
|
+
return undefined;
|
|
50
|
+
return others[0];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Poll until a successor of `departed` is live, or the budget runs out. Null on timeout — never
|
|
54
|
+
* throws, because failing to follow is a legitimate `observation_lost`, not an error.
|
|
55
|
+
*/
|
|
56
|
+
export async function awaitDocumentSuccessor(sessions, departed, timeoutMs, clock = REAL_CLOCK) {
|
|
57
|
+
const deadline = clock.now() + timeoutMs;
|
|
58
|
+
const departedIdentity = identityOf(departed);
|
|
59
|
+
for (;;) {
|
|
60
|
+
const sameId = sessions.get(departed.id);
|
|
61
|
+
if (sameId !== undefined && sameId !== departed)
|
|
62
|
+
return sameId;
|
|
63
|
+
// The departed session often still sits in the registry until the socket close lands. Leave it
|
|
64
|
+
// out of the pick, or same-id-first would keep returning the page that just unloaded.
|
|
65
|
+
const picked = pickDocumentSuccessor(sessions
|
|
66
|
+
.all()
|
|
67
|
+
.filter((s) => s !== departed)
|
|
68
|
+
.map(identityOf), departedIdentity);
|
|
69
|
+
if (picked !== undefined) {
|
|
70
|
+
const live = sessions.get(picked.id);
|
|
71
|
+
if (live !== undefined && live !== departed)
|
|
72
|
+
return live;
|
|
73
|
+
}
|
|
74
|
+
if (clock.now() >= deadline)
|
|
75
|
+
return null;
|
|
76
|
+
await clock.sleep(POLL_MS);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { WebSocket } from 'ws';
|
|
2
2
|
import type { ImpactSnapshot } from '@reticlehq/core';
|
|
3
3
|
import { LastAct } from './last-act.js';
|
|
4
|
+
import { GapLedger } from '../honesty/gap-ledger.js';
|
|
5
|
+
import { CaptureLedger } from '../honesty/feature-capture.js';
|
|
4
6
|
import { type PageRuntime } from './command-timeout.js';
|
|
5
7
|
import { type SessionHealth } from './session-health.js';
|
|
6
8
|
export type { SessionHealth };
|
|
7
|
-
import { PresenterTone, SessionState, type BrowserBrand, type CommandResult, type HelloMessage, type HumanControlData, type ReticleEvent } from '@reticlehq/core';
|
|
9
|
+
import { PresenterTone, SessionState, type BrowserBrand, type CommandResult, type JournalAction, type HelloMessage, type HumanControlData, type ReticleEvent } from '@reticlehq/core';
|
|
8
10
|
import type { JournalReader, JournalRecorder } from '../journal/journal-recorder.js';
|
|
9
11
|
import { type EventQueryOptions } from '../journal/journal-query.js';
|
|
10
12
|
import { type AmbientCounts } from '../journal/ambient.js';
|
|
@@ -62,6 +64,10 @@ export declare class Session {
|
|
|
62
64
|
info(): SessionInfo;
|
|
63
65
|
/** Wall-clock age of the session in milliseconds. */
|
|
64
66
|
staleMs(): number;
|
|
67
|
+
/** The document the most recent stamped event was observed under. See `#documentId`. */
|
|
68
|
+
get currentDocumentId(): string | undefined;
|
|
69
|
+
/** The edit epoch the most recent stamped event was observed under. See `#editEpoch`. */
|
|
70
|
+
get currentEditEpoch(): number | undefined;
|
|
65
71
|
/** Re-stamp an incoming event with server-relative time, buffer it, and fan out. */
|
|
66
72
|
pushEvent(event: ReticleEvent, byteSize?: number): void;
|
|
67
73
|
recordActedRef(ref: string): void;
|
|
@@ -99,8 +105,28 @@ export declare class Session {
|
|
|
99
105
|
* action id. Ids are minted independently of command correlation ids so the journal is self-contained.
|
|
100
106
|
*/
|
|
101
107
|
beginAction(tool: string, args: Record<string, unknown>): string;
|
|
108
|
+
/**
|
|
109
|
+
* Record one action WITHOUT opening an attribution window — for a tool that returns a verdict but
|
|
110
|
+
* drives nothing, so no event it observes was caused by it. See `JournalRecorder.recordAction`.
|
|
111
|
+
*/
|
|
112
|
+
recordAction(tool: string, args: Record<string, unknown>, effect?: unknown): string;
|
|
113
|
+
/**
|
|
114
|
+
* How many actions this session has dispatched.
|
|
115
|
+
*
|
|
116
|
+
* The counter that mints journal action ids, not a second one — so it equals the journal's own
|
|
117
|
+
* length, and `reticle_context`'s `step`, whenever this session journals at all.
|
|
118
|
+
*/
|
|
119
|
+
get actionCount(): number;
|
|
102
120
|
/** Close the active action window, persisting its action record with the settle outcome. */
|
|
103
121
|
finishAction(effect?: unknown, settled?: boolean, settledInMs?: number): void;
|
|
122
|
+
/**
|
|
123
|
+
* The durable action ledger for this session — the ledger the run context is folded out of.
|
|
124
|
+
*
|
|
125
|
+
* `[]` when nothing journals this session (journaling disabled, or an id that is not a safe path
|
|
126
|
+
* segment). An absent ledger reads as an empty run, never as a missing one: the fold simply has
|
|
127
|
+
* nothing to say, which is the honest answer.
|
|
128
|
+
*/
|
|
129
|
+
readJournalActions(): Promise<JournalAction[]>;
|
|
104
130
|
/** Flush any buffered journal events to disk (call on session end). */
|
|
105
131
|
flushJournal(): Promise<void>;
|
|
106
132
|
eventsSince(cursor: number): ReticleEvent[];
|
|
@@ -111,6 +137,19 @@ export declare class Session {
|
|
|
111
137
|
* from it, and the split between those two tool calls is the whole reason it exists.
|
|
112
138
|
*/
|
|
113
139
|
readonly lastAct: LastAct;
|
|
140
|
+
/**
|
|
141
|
+
* What this app still cannot tell Reticle, as of the most recent verdict.
|
|
142
|
+
*
|
|
143
|
+
* Lives on the session because that is the scope the question belongs to: "am I done with this
|
|
144
|
+
* app" is a session question, and a gap closed halfway through must not follow the agent to the
|
|
145
|
+
* end of it.
|
|
146
|
+
*/
|
|
147
|
+
readonly gaps: GapLedger;
|
|
148
|
+
/**
|
|
149
|
+
* The read-only calls the journal does not keep, so "was `reticle_context` ever called" has
|
|
150
|
+
* something to fold. See honesty/feature-capture.ts — it is the only state that instrument adds.
|
|
151
|
+
*/
|
|
152
|
+
readonly capture: CaptureLedger;
|
|
114
153
|
/**
|
|
115
154
|
* Stamp the wall-clock of the latest AGENT command (called whenever a tool resolves this session).
|
|
116
155
|
* If the reaper had auto-ended the session, a fresh command means the agent is alive again, so the
|