@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
|
@@ -129,8 +129,10 @@ const NUXT_STALE_BUNDLE = 'This project is Nuxt: the most likely cause by a dist
|
|
|
129
129
|
const RESTARTED_LEAD = 'no browser session connected. This daemon has served none since it started, but it is a NEW ' +
|
|
130
130
|
'process — an app for this project HAS connected on this port before, which is recorded ' +
|
|
131
131
|
'durably. So the wiring is correct and the port is correct: what is failing now is the SDK ' +
|
|
132
|
-
'reaching initialise on the page.
|
|
133
|
-
|
|
132
|
+
'reaching initialise on the page.';
|
|
133
|
+
/** Why the obvious move is the wrong one here. Reasoning, so it rides in the detail. */
|
|
134
|
+
const DO_NOT_REINSTALL = 'Re-running the install is the wrong move here: it cannot help on a project that has ' +
|
|
135
|
+
'demonstrably connected, and it can overwrite a working config.';
|
|
134
136
|
/**
|
|
135
137
|
* The ports the scan actually covers, rendered for the message.
|
|
136
138
|
*
|
|
@@ -259,10 +261,8 @@ function configsElsewhereClause(facts) {
|
|
|
259
261
|
.map((c) => (c.projectId === undefined ? c.directory : `${c.directory} ('${c.projectId}')`))
|
|
260
262
|
.join(', ');
|
|
261
263
|
return (`A \`.reticle.json\` WAS found outside this daemon's directory: ${named}. So the app is wired ` +
|
|
262
|
-
'and this is a SCOPE problem, not an install problem
|
|
263
|
-
'
|
|
264
|
-
"directory. Restart the daemon from the app's directory (or point it there) rather than " +
|
|
265
|
-
'installing anything.');
|
|
264
|
+
'and this is a SCOPE problem, not an install problem. Restart the daemon from the ' +
|
|
265
|
+
"app's directory (or point it there) rather than installing anything.");
|
|
266
266
|
}
|
|
267
267
|
/** Where we looked, when we found nothing. An absence nobody can check is not evidence. */
|
|
268
268
|
function searchedClause(facts) {
|
|
@@ -327,13 +327,12 @@ export function explainNoSession(facts) {
|
|
|
327
327
|
if (true === facts.authRefused) {
|
|
328
328
|
return reason(NoSessionReason.AUTH_REFUSED, 'no browser session connected, and the reason is not the app: this daemon REFUSED the last ' +
|
|
329
329
|
'page that dialled it, because the pairing token it presented is not the one this daemon ' +
|
|
330
|
-
'holds. Only an SDK dials the bridge, so the wiring is correct and the app is running.
|
|
331
|
-
'token is read by the build plugin when the dev server STARTS, so the usual cause is a ' +
|
|
332
|
-
'daemon restarted (or a `~/.reticle` cleared) after the dev server was already up
|
|
333
|
-
'
|
|
334
|
-
'
|
|
335
|
-
'
|
|
336
|
-
'The page says the same thing in the browser console.');
|
|
330
|
+
'holds. Only an SDK dials the bridge, so the wiring is correct and the app is running. ' +
|
|
331
|
+
'Restart the dev server so it picks up the current token.', 'The token is read by the build plugin when the dev server STARTS, so the usual cause is a ' +
|
|
332
|
+
'daemon restarted (or a `~/.reticle` cleared) after the dev server was already up. If the ' +
|
|
333
|
+
'two were started from different environments — an editor spawning the MCP server globally ' +
|
|
334
|
+
'while the dev server runs from a shell — they can be reading different `~/.reticle` ' +
|
|
335
|
+
'directories instead. The page says the same thing in the browser console.');
|
|
337
336
|
}
|
|
338
337
|
if (everConnected) {
|
|
339
338
|
// A reaped lease first, because it is the one cause with POSITIVE evidence behind it. Calling
|
|
@@ -344,15 +343,13 @@ export function explainNoSession(facts) {
|
|
|
344
343
|
// (#611), so this branch is only reached when the thing that vanished WAS the lease.
|
|
345
344
|
if (true === facts.leaseExpired) {
|
|
346
345
|
return reason(NoSessionReason.LEASE_EXPIRED, 'no browser session connected, but one WAS connected to this daemon earlier, so the wiring ' +
|
|
347
|
-
'is correct. The session that went away was a pooled lease and it aged out
|
|
348
|
-
'headless context, not a human tab, and it takes its cookies with it
|
|
349
|
-
|
|
350
|
-
`carry on.${alreadyListeningClause(listening)} ${RETRY}`);
|
|
346
|
+
'is correct. The session that went away was a pooled lease and it aged out. Re-acquire ' +
|
|
347
|
+
`with reticle_lease {action:"acquire", url} and carry on. ${RETRY}`, 'A lease is a headless context, not a human tab, and it takes its cookies with it, so an ' +
|
|
348
|
+
`authenticated app needs signing in again.${alreadyListeningClause(listening)}`);
|
|
351
349
|
}
|
|
352
350
|
return reason(NoSessionReason.TAB_GONE, 'no browser session connected, but one WAS connected to this daemon earlier, so the wiring ' +
|
|
353
351
|
`is correct. ${tabGoneWhat(facts.lastKnownUrl)} Ask the human to reopen ` +
|
|
354
|
-
`the app (or run ${OPEN_CMD_BARE}), or reload the tab
|
|
355
|
-
`${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`);
|
|
352
|
+
`the app (or run ${OPEN_CMD_BARE}), or reload the tab. ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, alreadyListeningClause(listening).trim());
|
|
356
353
|
}
|
|
357
354
|
// Ranked ahead of every "we have never seen one" branch below, because it is the one fact that
|
|
358
355
|
// OUTRANKS them: those branches all reason from an absence, and this reasons from a recorded
|
|
@@ -363,15 +360,16 @@ export function explainNoSession(facts) {
|
|
|
363
360
|
? `${unattributedListeners(listening)} If the dev server is not running, start it first ` +
|
|
364
361
|
'(the command is in `next_action`).'
|
|
365
362
|
: unattributedListeners(listening);
|
|
366
|
-
return reason(NoSessionReason.APP_NOT_REOPENED, `${RESTARTED_LEAD} ${OPEN_THE_APP} ${
|
|
363
|
+
return reason(NoSessionReason.APP_NOT_REOPENED, `${RESTARTED_LEAD} ${OPEN_THE_APP} ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, `${DO_NOT_REINSTALL} ${listeners} ${rankedCauses(facts)}`);
|
|
367
364
|
}
|
|
368
365
|
// A config found elsewhere outranks every "you may have no SDK" branch below: those reason from
|
|
369
366
|
// an absence in ONE directory, and this is a file we read in another. Ranked here so the reader is
|
|
370
367
|
// never sent through an install on a project that has demonstrably been installed.
|
|
371
368
|
if (!initialized && (facts.configsElsewhere ?? []).length > 0) {
|
|
372
369
|
return reason(NoSessionReason.CONFIG_ELSEWHERE, 'no browser session connected, and this daemon has never seen one. ' +
|
|
373
|
-
`${configsElsewhereClause(facts)} ${
|
|
374
|
-
|
|
370
|
+
`${configsElsewhereClause(facts)} ${OPEN_THE_APP} ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, 'This daemon was started somewhere that is not the project, which is the normal outcome when ' +
|
|
371
|
+
`an editor launches it from your home directory. ${unattributedListeners(listening)} ` +
|
|
372
|
+
`${rankedCauses(facts)}`);
|
|
375
373
|
}
|
|
376
374
|
if (0 === listening.length) {
|
|
377
375
|
// Lead with the stronger EVIDENCE, and do not overstate what it is.
|
|
@@ -390,54 +388,62 @@ export function explainNoSession(facts) {
|
|
|
390
388
|
// is expected rather than diagnostic.
|
|
391
389
|
if (!initialized) {
|
|
392
390
|
return reason(NoSessionReason.NO_LISTENER_NO_CONFIG, 'no browser session connected. Two things to weigh, and neither of them is proof. ' +
|
|
393
|
-
`(1) ${slowListenerClause(facts)}Nothing is listening on the ports Reticle scans
|
|
394
|
-
'may not be running
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
'running.
|
|
398
|
-
'
|
|
399
|
-
`
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
'while the app lives in a subdirectory, and an app wired by the Vite or Babel plugin ' +
|
|
404
|
-
`carries the SDK without that file at all.${searchedClause(facts)}${siblingListenerClause(facts)} ${leaseAdvice(URL_THEN_LEASE, facts)} ${RETRY}`);
|
|
391
|
+
`(1) ${slowListenerClause(facts)}Nothing is listening on the ports Reticle scans, so the ` +
|
|
392
|
+
'dev server may not be running: START IT YOURSELF, in the background, using the command ' +
|
|
393
|
+
'in `next_action`, and tell the human in one line that it is running. ' +
|
|
394
|
+
`(2) There is no \`.reticle.json\` in ${where}, which is the file ${INIT_CMD} writes, so ` +
|
|
395
|
+
`the app may carry no Reticle SDK. Check the app's OWN directory before re-running it. ${RETRY}`, `The scan covers ${SCANNED_PORTS} and nothing else, so a server on any other port is ` +
|
|
396
|
+
'invisible to it: if the app IS running, ask for its URL rather than assuming it is down, ' +
|
|
397
|
+
`and open it with ${OPEN_CMD}. The missing config is not proof either — in a monorepo the ` +
|
|
398
|
+
'daemon often runs at the root while the app lives in a subdirectory, and an app wired by ' +
|
|
399
|
+
'the Vite or Babel plugin carries the SDK without that file at all.' +
|
|
400
|
+
`${searchedClause(facts)}${siblingListenerClause(facts)} ${leaseAdvice(URL_THEN_LEASE, facts)}`);
|
|
405
401
|
}
|
|
406
402
|
return reason(NoSessionReason.NO_LISTENER, `${stallClause(facts)}no browser session connected, and this daemon has never seen one. ${OPEN_THE_APP} ` +
|
|
407
|
-
'Nothing is listening on the ports Reticle scans ' +
|
|
408
|
-
|
|
409
|
-
'
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
'
|
|
403
|
+
'Nothing is listening on the ports Reticle scans either, and the usual reason is a dev ' +
|
|
404
|
+
'server that is not running: start it yourself in the background with the command in ' +
|
|
405
|
+
'`next_action`, tell the human in one line that it is running, then open the app in a ' +
|
|
406
|
+
`browser. ${RETRY}`,
|
|
407
|
+
// The caveat is in the detail rather than omitted because the scan is NARROW: an empty result
|
|
408
|
+
// is not proof of absence. Reported twice, once against a server answering 200 on :7699 and
|
|
409
|
+
// once against one on :5000 under a custom hostname. The common defaults are in the scanned
|
|
410
|
+
// set, which narrows the gap and cannot close it — anything passed to `--port` is invisible.
|
|
411
|
+
`The scan covers ${SCANNED_PORTS} and nothing else, so it is not proof: a server on any other ` +
|
|
412
|
+
'port is invisible to it. If the app IS running, ask the human for its URL rather than ' +
|
|
417
413
|
// Deliberately NOT offering reticle_lease here, and a test pins that: a lease opens a URL, and
|
|
418
414
|
// if nothing is listening there is nothing at any URL to open. Asking for the real one is the
|
|
419
415
|
// only move that can recover the :7699 case.
|
|
420
|
-
`
|
|
421
|
-
|
|
416
|
+
`assuming it is down. The \`next_action\` command is read from this project's own scripts, ` +
|
|
417
|
+
'and says so rather than guessing when there is none. ' +
|
|
418
|
+
`${leaseAdvice(URL_THEN_LEASE, facts)}${siblingListenerClause(facts)}`);
|
|
422
419
|
}
|
|
423
420
|
if (!initialized) {
|
|
424
421
|
return reason(NoSessionReason.NO_CONFIG, 'no browser session connected, and this daemon has never seen one. ' +
|
|
425
|
-
`What was actually checked: there is no \`.reticle.json\` in ${where}
|
|
426
|
-
`${INIT_CMD} writes, so the app may carry no Reticle SDK
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
`
|
|
430
|
-
|
|
431
|
-
|
|
422
|
+
`What was actually checked: there is no \`.reticle.json\` in ${where}, which is the file ` +
|
|
423
|
+
`${INIT_CMD} writes, so the app may carry no Reticle SDK. Check the app's OWN directory: ` +
|
|
424
|
+
`if it has no config, run ${INIT_CMD} ` +
|
|
425
|
+
'there and restart the dev server; if it has one, the app is wired and simply has no page ' +
|
|
426
|
+
`open — ${OPEN_CMD}. ${RETRY}`, 'That absence is not proof: the same one is expected in a monorepo whose daemon runs at the ' +
|
|
427
|
+
'root while the app lives in a subdirectory, or in an app wired by the Vite or Babel ' +
|
|
428
|
+
`plugin. ${unattributedListeners(listening)}${searchedClause(facts)}${siblingListenerClause(facts)}`);
|
|
432
429
|
}
|
|
433
430
|
return reason(NoSessionReason.SDK_NOT_REACHING_DAEMON, `${stallClause(facts)}no browser session connected, and this daemon has never seen one for this project, which is ` +
|
|
434
|
-
`wired for Reticle. ${OPEN_THE_APP}
|
|
435
|
-
|
|
436
|
-
`this daemon (on ${String(port)}). ${rankedCauses(facts)} ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`);
|
|
431
|
+
`wired for Reticle. ${OPEN_THE_APP} If the page IS open and still does not appear, the SDK ` +
|
|
432
|
+
`is not reaching this daemon (on ${String(port)}). ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, `${unattributedListeners(listening)} ${rankedCauses(facts)}`);
|
|
437
433
|
}
|
|
438
|
-
/**
|
|
439
|
-
|
|
440
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Pair a branch's verdict with its prose, so neither can be produced without the other.
|
|
436
|
+
*
|
|
437
|
+
* TWO pieces of prose now. `message` is the lead: what is true and what to do about it, in about
|
|
438
|
+
* four sentences. `detail` is the differential behind it -- the port-scan caveat, the ranked causes,
|
|
439
|
+
* the lease offer -- which is the part that was making this five hundred words in one paragraph.
|
|
440
|
+
*
|
|
441
|
+
* Split rather than deleted. Almost every clause in `detail` exists because somebody was sent the
|
|
442
|
+
* wrong way without it, and the reader who needs it is the agent about to spend a drive on a guess.
|
|
443
|
+
* The reader who does not is the person watching `reticle init`, who gets the lead.
|
|
444
|
+
*/
|
|
445
|
+
function reason(code, message, detail) {
|
|
446
|
+
return { reason: code, message, ...(undefined === detail ? {} : { detail }) };
|
|
441
447
|
}
|
|
442
448
|
/**
|
|
443
449
|
* The message alone, for the callers that render it.
|
|
@@ -446,5 +452,6 @@ function reason(code, message) {
|
|
|
446
452
|
* pair would spread a telemetry concern across every error path that shows a user this text.
|
|
447
453
|
*/
|
|
448
454
|
export function diagnoseNoSession(facts) {
|
|
449
|
-
|
|
455
|
+
const { message, detail } = explainNoSession(facts);
|
|
456
|
+
return undefined === detail ? message : `${message} ${detail}`;
|
|
450
457
|
}
|
|
@@ -140,8 +140,9 @@ export function nextActionFor(facts) {
|
|
|
140
140
|
"own. Point the plugin at the daemon's copy — mount `~/.reticle/pairing-token` into the " +
|
|
141
141
|
`container read-only and set \`${ReticleEnv.PAIRING_TOKEN_DIR}\` to the directory holding ` +
|
|
142
142
|
'it — then ' +
|
|
143
|
-
'restart the dev server, because the token is inlined when Vite resolves its config
|
|
144
|
-
'
|
|
143
|
+
'restart the dev server, because the token is inlined when Vite resolves its config and ' +
|
|
144
|
+
'kept in the prebundled module. Reloading the page serves that same token, whether the ' +
|
|
145
|
+
'dev server is on this machine or in a container.',
|
|
145
146
|
};
|
|
146
147
|
}
|
|
147
148
|
if (!facts.initialized && true === facts.authRefused) {
|
|
@@ -23,7 +23,7 @@ import { daemonsServingProjectElsewhere, splitBrainNote } from '../../command/da
|
|
|
23
23
|
import { stallUptime } from './stall-clock.js';
|
|
24
24
|
import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
|
|
25
25
|
import { findOccupiedSiblings } from '../../command/cli/ports/sibling-ports.js';
|
|
26
|
-
import {
|
|
26
|
+
import { isAuthRefusalReason } from '../bridge/auth-failure-reason.js';
|
|
27
27
|
/** Slow enough to be free, fast enough that a dev server started 15s ago is already reflected. */
|
|
28
28
|
const REFRESH_MS = 15_000;
|
|
29
29
|
/** The URL an auto-attach opens. `localhost` for the same reason the probe uses it: both stacks. */
|
|
@@ -208,7 +208,7 @@ export function startNoSessionWatch(options) {
|
|
|
208
208
|
/**
|
|
209
209
|
* Is a refusal on the pairing token the CURRENT state of the bridge?
|
|
210
210
|
*
|
|
211
|
-
* The bridge records the refusal (
|
|
211
|
+
* The bridge records the refusal (the sentence it closed the socket with) and nothing read it
|
|
212
212
|
* as a DIAGNOSIS. It is the one fact that proves an app is running and instrumented: only an SDK
|
|
213
213
|
* dials the bridge, so a refused hello means the wiring works and this daemon would not serve it.
|
|
214
214
|
*
|
|
@@ -223,7 +223,7 @@ export function startNoSessionWatch(options) {
|
|
|
223
223
|
* recorded no refusal, which falls through to the behaviour that was there before -- the safe
|
|
224
224
|
* direction for a fact whose job is to SUPPRESS advice rather than to add any.
|
|
225
225
|
*/
|
|
226
|
-
const lastCloseWasAuthFailure = () => options.sessions.lastClosure?.()?.reason
|
|
226
|
+
const lastCloseWasAuthFailure = () => isAuthRefusalReason(options.sessions.lastClosure?.()?.reason) &&
|
|
227
227
|
true !== options.sessions.connectedSinceLastClosure?.();
|
|
228
228
|
const nextAction = (scope) => {
|
|
229
229
|
const split = splitBrain();
|
|
@@ -307,19 +307,23 @@ export function startNoSessionWatch(options) {
|
|
|
307
307
|
...(0 === siblingListeners.length ? {} : { siblingListeners }),
|
|
308
308
|
});
|
|
309
309
|
};
|
|
310
|
-
|
|
310
|
+
// The lead, then the literal command, then the differential. Both renderings consume the same
|
|
311
|
+
// scope facts so a discovered workspace config cannot become an `init` recommendation below it,
|
|
312
|
+
// and both come from ONE `explain()` call so they cannot describe different branches.
|
|
313
|
+
const rendered = () => {
|
|
311
314
|
const scope = projectScopeFacts();
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
315
|
+
const { message, detail } = explain();
|
|
316
|
+
const lead = `${message} ${renderNextAction(nextAction(scope))}${attachFailure ?? ''}`;
|
|
317
|
+
return { lead, full: undefined === detail ? lead : `${lead} ${detail}` };
|
|
318
|
+
};
|
|
319
|
+
options.sessions.setNoSessionHint(() => rendered().full);
|
|
320
|
+
options.sessions.setNoSessionLead(() => rendered().lead);
|
|
318
321
|
options.sessions.setNoSessionReason(() => explain().reason);
|
|
319
322
|
return () => {
|
|
320
323
|
clearInterval(timer);
|
|
321
324
|
options.sessions.setConnectionRecorder(undefined);
|
|
322
325
|
options.sessions.setNoSessionHint(undefined);
|
|
326
|
+
options.sessions.setNoSessionLead(undefined);
|
|
323
327
|
options.sessions.setNoSessionReason(undefined);
|
|
324
328
|
options.sessions.setNoSessionNextAction(undefined);
|
|
325
329
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands that may be re-issued against the connection that replaced this session.
|
|
3
|
+
*
|
|
4
|
+
* Reads only, and the boundary is the point. Re-reading a page costs nothing and answers the same
|
|
5
|
+
* question the caller asked. An act cannot be replayed: it may already have been dispatched in the
|
|
6
|
+
* page that went away, and performing it a second time behind the caller's back is a double submit
|
|
7
|
+
* nobody asked for. A replaced act still errors, and the caller's own retry — with the same id,
|
|
8
|
+
* which is still the right one — is the safe path.
|
|
9
|
+
*/
|
|
10
|
+
export declare const REBINDABLE_COMMANDS: ReadonlySet<string>;
|
|
11
|
+
/**
|
|
12
|
+
* How far to walk a chain of replacements before giving up.
|
|
13
|
+
*
|
|
14
|
+
* Generous for the real case (a page reloading a handful of times while a call is in flight) and
|
|
15
|
+
* small enough that a cycle costs nothing. The number is a backstop, not a policy: a chain longer
|
|
16
|
+
* than this means something is re-dialling in a loop, and that is a different problem.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MAX_SUCCESSOR_HOPS = 32;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a command in flight may be re-issued against the connection that REPLACED this session,
|
|
3
|
+
* and how far a chain of replacements is worth walking.
|
|
4
|
+
*
|
|
5
|
+
* One question, so one file. It left `session.ts` when that file crossed the thousand-line cap on a
|
|
6
|
+
* merge: two branches had each grown it by a few lines and the sum went over, which is the moment
|
|
7
|
+
* the rule says to split rather than to raise the number. These two constants and the reasoning
|
|
8
|
+
* behind them are the most self-contained thing in there — `session.ts` reads them twice and owns
|
|
9
|
+
* nothing else about rebinding.
|
|
10
|
+
*/
|
|
11
|
+
import { ReticleCommand } from '@reticlehq/core';
|
|
12
|
+
/**
|
|
13
|
+
* Commands that may be re-issued against the connection that replaced this session.
|
|
14
|
+
*
|
|
15
|
+
* Reads only, and the boundary is the point. Re-reading a page costs nothing and answers the same
|
|
16
|
+
* question the caller asked. An act cannot be replayed: it may already have been dispatched in the
|
|
17
|
+
* page that went away, and performing it a second time behind the caller's back is a double submit
|
|
18
|
+
* nobody asked for. A replaced act still errors, and the caller's own retry — with the same id,
|
|
19
|
+
* which is still the right one — is the safe path.
|
|
20
|
+
*/
|
|
21
|
+
export const REBINDABLE_COMMANDS = new Set([
|
|
22
|
+
ReticleCommand.SNAPSHOT,
|
|
23
|
+
ReticleCommand.QUERY,
|
|
24
|
+
ReticleCommand.MATCH,
|
|
25
|
+
ReticleCommand.INSPECT,
|
|
26
|
+
ReticleCommand.STATE_READ,
|
|
27
|
+
ReticleCommand.STORAGE_READ,
|
|
28
|
+
ReticleCommand.CAPABILITIES,
|
|
29
|
+
ReticleCommand.ANIMATIONS,
|
|
30
|
+
]);
|
|
31
|
+
/**
|
|
32
|
+
* How far to walk a chain of replacements before giving up.
|
|
33
|
+
*
|
|
34
|
+
* Generous for the real case (a page reloading a handful of times while a call is in flight) and
|
|
35
|
+
* small enough that a cycle costs nothing. The number is a backstop, not a policy: a chain longer
|
|
36
|
+
* than this means something is re-dialling in a loop, and that is a different problem.
|
|
37
|
+
*/
|
|
38
|
+
export const MAX_SUCCESSOR_HOPS = 32;
|
|
@@ -30,6 +30,10 @@ export declare class SessionManager {
|
|
|
30
30
|
count(): number;
|
|
31
31
|
/** Wire the diagnosis provider (daemon boot). Absent ⇒ the plain, static message. */
|
|
32
32
|
setNoSessionHint(hint: (() => string | undefined) | undefined): void;
|
|
33
|
+
/** Wire the short rendering. Registered from the same `explainNoSession` call as the hint. */
|
|
34
|
+
setNoSessionLead(lead: (() => string | undefined) | undefined): void;
|
|
35
|
+
/** The lead and the next action, for a surface a person reads. */
|
|
36
|
+
noSessionLead(): string | undefined;
|
|
33
37
|
/**
|
|
34
38
|
* The same diagnosis, for a caller that is ASKING about sessions rather than failing to get one.
|
|
35
39
|
*
|
|
@@ -4,6 +4,7 @@ import { declareDrivenRedactionKeys, forgetDrivenRedactionKeys, } from '../input
|
|
|
4
4
|
import { Session } from './session.js';
|
|
5
5
|
import { AttachmentHistory } from './presence/attachment-history.js';
|
|
6
6
|
import { pickDocumentSuccessor } from './session-successor.js';
|
|
7
|
+
import { SESSION_DISCONNECTED_REASON } from './facts/session-replaced.js';
|
|
7
8
|
/** The scheme://host:port of a URL, or undefined if it can't be parsed. Used to compare origins. */
|
|
8
9
|
export function originOf(url) {
|
|
9
10
|
if (url === undefined)
|
|
@@ -143,7 +144,7 @@ export class SessionManager {
|
|
|
143
144
|
remove(session) {
|
|
144
145
|
if (this.#sessions.get(session.id) !== session)
|
|
145
146
|
return false;
|
|
146
|
-
session.rejectAll(
|
|
147
|
+
session.rejectAll(SESSION_DISCONNECTED_REASON);
|
|
147
148
|
forgetDrivenRedactionKeys(session.id);
|
|
148
149
|
// Recorded, not forgotten: a session that comes back needs its gap measured, and a listing after
|
|
149
150
|
// the reconnect is exactly where that matters.
|
|
@@ -246,10 +247,27 @@ export class SessionManager {
|
|
|
246
247
|
* different branches. See no-session-watch.ts.
|
|
247
248
|
*/
|
|
248
249
|
#noSessionReason;
|
|
250
|
+
/**
|
|
251
|
+
* The same diagnosis with the differential left off: the lead and the next action, nothing else.
|
|
252
|
+
*
|
|
253
|
+
* Two renderings because there are two readers, and only one of them can spend the words. An
|
|
254
|
+
* agent about to burn a drive on a guess wants the ranked causes, the port-scan caveat and the
|
|
255
|
+
* lease offer. The person watching `reticle init` fail wants to know what is wrong and what to
|
|
256
|
+
* type, and was instead handed roughly five hundred words in one paragraph.
|
|
257
|
+
*/
|
|
258
|
+
#noSessionLead;
|
|
249
259
|
/** Wire the diagnosis provider (daemon boot). Absent ⇒ the plain, static message. */
|
|
250
260
|
setNoSessionHint(hint) {
|
|
251
261
|
this.#noSessionHint = hint;
|
|
252
262
|
}
|
|
263
|
+
/** Wire the short rendering. Registered from the same `explainNoSession` call as the hint. */
|
|
264
|
+
setNoSessionLead(lead) {
|
|
265
|
+
this.#noSessionLead = lead;
|
|
266
|
+
}
|
|
267
|
+
/** The lead and the next action, for a surface a person reads. */
|
|
268
|
+
noSessionLead() {
|
|
269
|
+
return this.#noSessionLead?.();
|
|
270
|
+
}
|
|
253
271
|
/**
|
|
254
272
|
* The same diagnosis, for a caller that is ASKING about sessions rather than failing to get one.
|
|
255
273
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WebSocket } from 'ws';
|
|
2
|
-
import type
|
|
2
|
+
import { type ChannelId, type ImpactSnapshot, type ProjectId } from '@reticlehq/core';
|
|
3
3
|
import type { HandshakeFacts } from './facts/handshake-facts.js';
|
|
4
4
|
import { LastAct } from './last-act.js';
|
|
5
5
|
import { GapLedger } from '@reticlehq/engine/evidence/gap-ledger.js';
|
|
@@ -7,7 +7,7 @@ import { CaptureLedger } from '../../surface/tools/feature-capture.js';
|
|
|
7
7
|
import { type PageRuntime } from './timing/command-timeout.js';
|
|
8
8
|
import { type SessionHealth } from './session-health.js';
|
|
9
9
|
export type { SessionHealth };
|
|
10
|
-
import { PresenterTone, SessionState, type BrowserBrand, type CommandResult, type JournalAction, type HelloMessage, type HumanControlData, type ReticleEvent } from '@reticlehq/core';
|
|
10
|
+
import { PresenterTone, SessionState, type BrowserBrand, type CommandResult, type JournalAction, type JournalWriteLoss, type HelloMessage, type HumanControlData, type ReticleEvent } from '@reticlehq/core';
|
|
11
11
|
import type { JournalReader, JournalRecorder } from '../../memory/journal/journal-recorder.js';
|
|
12
12
|
import { type EventQueryOptions } from '../../memory/journal/journal-query.js';
|
|
13
13
|
import { type AmbientCounts } from '@reticlehq/engine/window/ambient.js';
|
|
@@ -25,8 +25,14 @@ type Clock = () => number;
|
|
|
25
25
|
export declare class Session implements HandshakeFacts {
|
|
26
26
|
#private;
|
|
27
27
|
readonly id: string;
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Stable build-stamped project identity; undefined for SDKs old enough to omit it.
|
|
30
|
+
*
|
|
31
|
+
* MINTED HERE. This is one of the two places a projectId enters the daemon with its provenance
|
|
32
|
+
* intact: the field is `projectId` on a zod-parsed HELLO, so the page said what it is. Every
|
|
33
|
+
* downstream reader takes the brand from this property rather than re-blessing a string.
|
|
34
|
+
*/
|
|
35
|
+
readonly projectId: ProjectId | undefined;
|
|
30
36
|
/**
|
|
31
37
|
* The `.reticle` directory this session's evidence belongs in — impact counters AND capsules.
|
|
32
38
|
*
|
|
@@ -118,12 +124,13 @@ export declare class Session implements HandshakeFacts {
|
|
|
118
124
|
*/
|
|
119
125
|
setJournal(recorder: JournalRecorder, reader?: JournalReader): void;
|
|
120
126
|
/**
|
|
121
|
-
* Journal-backed event query
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* evidence — the substrate's whole point. The sync `eventsSince`/`window` stay for the hot path.
|
|
127
|
+
* Journal-backed event query — the rule itself lives in `readQueryEvents`, beside the merge and
|
|
128
|
+
* filter it is made of. This is how "what did action N cause" is answered after the buffer has
|
|
129
|
+
* dropped the evidence. The sync `eventsSince`/`window` stay for the hot path.
|
|
125
130
|
*/
|
|
126
131
|
queryEvents(options: EventQueryOptions): Promise<ReticleEvent[]>;
|
|
132
|
+
/** What the durable ledger could not WRITE — see `readJournalWriteLoss` for the rule and why. */
|
|
133
|
+
journalWriteLoss(): Promise<JournalWriteLoss | undefined>;
|
|
127
134
|
/**
|
|
128
135
|
* Open an action-attribution window: events observed until finishAction attribute to the returned
|
|
129
136
|
* action id. Ids are minted independently of command correlation ids so the journal is self-contained.
|
|
@@ -200,9 +207,13 @@ export declare class Session implements HandshakeFacts {
|
|
|
200
207
|
dropped: number;
|
|
201
208
|
};
|
|
202
209
|
/**
|
|
203
|
-
* Did the
|
|
210
|
+
* Did the EVENT STORE lose scarce evidence from a window opened at `cursor`? The input to whether a
|
|
204
211
|
* verdict's capture was clean — see `RingBuffer.lostSince`, and never the raw drop counter, which
|
|
205
|
-
* moves for the age and churn evictions that every live page produces continuously.
|
|
212
|
+
* moves for the age and churn evictions that every live page produces continuously. The journal is
|
|
213
|
+
* the other half of that store, so a durable read that could not reach back to `cursor` lost
|
|
214
|
+
* exactly what an eviction would have. Both boundaries are INCLUSIVE (`t` is a millisecond many
|
|
215
|
+
* records share) and an absent `lostThroughT` impeaches every window rather than none — the
|
|
216
|
+
* conservative direction, both times.
|
|
206
217
|
*/
|
|
207
218
|
lostSince(cursor: number): boolean;
|
|
208
219
|
onEvent(listener: (event: ReticleEvent) => void): () => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asProjectId } from '@reticlehq/core';
|
|
1
2
|
import { refusedResult } from './page-commands/undeclared-command.js';
|
|
2
3
|
import { recordImpact } from '../../memory/impact/impact-recorder.js';
|
|
3
4
|
import { LastAct } from './last-act.js';
|
|
@@ -13,7 +14,7 @@ import { span } from '../../trace.js';
|
|
|
13
14
|
import { keepCallerContextInDaemon, noteToDaemonLog } from './daemon-answers.js';
|
|
14
15
|
import { isKnownRealm, EventType, HumanControlDataSchema, HumanControlKind, HumanMarkDataSchema, ReticleCommand, MessageKind, parseEventPayload, PresenterTone, SESSION_HEALTH, SESSION_LEASE, SESSION_LIFECYCLE, SessionState, } from '@reticlehq/core';
|
|
15
16
|
import { RingBuffer } from '@reticlehq/engine/window/ring-buffer.js';
|
|
16
|
-
import {
|
|
17
|
+
import { readJournalWriteLoss, readQueryEvents, } from '../../memory/journal/journal-query.js';
|
|
17
18
|
import {} from '@reticlehq/engine/window/ambient.js';
|
|
18
19
|
import { ObservedState } from './facts/observed-state.js';
|
|
19
20
|
import { recordBrowserLatency, recordSdkFailure } from '../../telemetry/session-metrics.js';
|
|
@@ -23,36 +24,10 @@ import { buildSessionRecommendation } from './presence/session-recommendation.js
|
|
|
23
24
|
import { buildPresenterArgs } from './human/presenter-args.js';
|
|
24
25
|
import { buildSessionLease } from './presence/session-lease.js';
|
|
25
26
|
import { buildSessionInfo } from './session-info.js';
|
|
27
|
+
import { MAX_SUCCESSOR_HOPS, REBINDABLE_COMMANDS } from './rebind.js';
|
|
26
28
|
const DEFAULT_COMMAND_TIMEOUT_MS = 8000;
|
|
27
29
|
/** Prefix on correlated command ids (c1, c2, …) — distinguishes them from mark ids. */
|
|
28
30
|
const COMMAND_ID_PREFIX = 'c';
|
|
29
|
-
/**
|
|
30
|
-
* Commands that may be re-issued against the connection that replaced this session.
|
|
31
|
-
*
|
|
32
|
-
* Reads only, and the boundary is the point. Re-reading a page costs nothing and answers the same
|
|
33
|
-
* question the caller asked. An act cannot be replayed: it may already have been dispatched in the
|
|
34
|
-
* page that went away, and performing it a second time behind the caller's back is a double submit
|
|
35
|
-
* nobody asked for. A replaced act still errors, and the caller's own retry — with the same id,
|
|
36
|
-
* which is still the right one — is the safe path.
|
|
37
|
-
*/
|
|
38
|
-
/**
|
|
39
|
-
* How far to walk a chain of replacements before giving up.
|
|
40
|
-
*
|
|
41
|
-
* Generous for the real case (a page reloading a handful of times while a call is in flight) and
|
|
42
|
-
* small enough that a cycle costs nothing. The number is a backstop, not a policy: a chain longer
|
|
43
|
-
* than this means something is re-dialling in a loop, and that is a different problem.
|
|
44
|
-
*/
|
|
45
|
-
const MAX_SUCCESSOR_HOPS = 32;
|
|
46
|
-
const REBINDABLE_COMMANDS = new Set([
|
|
47
|
-
ReticleCommand.SNAPSHOT,
|
|
48
|
-
ReticleCommand.QUERY,
|
|
49
|
-
ReticleCommand.MATCH,
|
|
50
|
-
ReticleCommand.INSPECT,
|
|
51
|
-
ReticleCommand.STATE_READ,
|
|
52
|
-
ReticleCommand.STORAGE_READ,
|
|
53
|
-
ReticleCommand.CAPABILITIES,
|
|
54
|
-
ReticleCommand.ANIMATIONS,
|
|
55
|
-
]);
|
|
56
31
|
/** Prefix on minted action ids (a1, a2, …) — the journal's action identity, independent of commands. */
|
|
57
32
|
const ACTION_ID_PREFIX = 'a';
|
|
58
33
|
/**
|
|
@@ -69,7 +44,13 @@ const WS_OPEN = 1;
|
|
|
69
44
|
*/
|
|
70
45
|
export class Session {
|
|
71
46
|
id;
|
|
72
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Stable build-stamped project identity; undefined for SDKs old enough to omit it.
|
|
49
|
+
*
|
|
50
|
+
* MINTED HERE. This is one of the two places a projectId enters the daemon with its provenance
|
|
51
|
+
* intact: the field is `projectId` on a zod-parsed HELLO, so the page said what it is. Every
|
|
52
|
+
* downstream reader takes the brand from this property rather than re-blessing a string.
|
|
53
|
+
*/
|
|
73
54
|
projectId;
|
|
74
55
|
/**
|
|
75
56
|
* The `.reticle` directory this session's evidence belongs in — impact counters AND capsules.
|
|
@@ -145,7 +126,7 @@ export class Session {
|
|
|
145
126
|
#editEpoch;
|
|
146
127
|
constructor(hello, socket, clock) {
|
|
147
128
|
this.id = hello.sessionId;
|
|
148
|
-
this.projectId = hello.projectId;
|
|
129
|
+
this.projectId = hello.projectId === undefined ? undefined : asProjectId(hello.projectId);
|
|
149
130
|
this.url = hello.url;
|
|
150
131
|
this.title = hello.title;
|
|
151
132
|
this.adapters = hello.adapters;
|
|
@@ -408,17 +389,16 @@ export class Session {
|
|
|
408
389
|
this.#journalReader = reader;
|
|
409
390
|
}
|
|
410
391
|
/**
|
|
411
|
-
* Journal-backed event query
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
* evidence — the substrate's whole point. The sync `eventsSince`/`window` stay for the hot path.
|
|
392
|
+
* Journal-backed event query — the rule itself lives in `readQueryEvents`, beside the merge and
|
|
393
|
+
* filter it is made of. This is how "what did action N cause" is answered after the buffer has
|
|
394
|
+
* dropped the evidence. The sync `eventsSince`/`window` stay for the hot path.
|
|
415
395
|
*/
|
|
416
396
|
async queryEvents(options) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
return
|
|
397
|
+
return readQueryEvents(this.#journalReader, this.#buffer, options);
|
|
398
|
+
}
|
|
399
|
+
/** What the durable ledger could not WRITE — see `readJournalWriteLoss` for the rule and why. */
|
|
400
|
+
async journalWriteLoss() {
|
|
401
|
+
return readJournalWriteLoss(this.#journalReader, this.#buffer);
|
|
422
402
|
}
|
|
423
403
|
/**
|
|
424
404
|
* Open an action-attribution window: events observed until finishAction attribute to the returned
|
|
@@ -541,12 +521,19 @@ export class Session {
|
|
|
541
521
|
return this.#buffer.bufferHealth();
|
|
542
522
|
}
|
|
543
523
|
/**
|
|
544
|
-
* Did the
|
|
524
|
+
* Did the EVENT STORE lose scarce evidence from a window opened at `cursor`? The input to whether a
|
|
545
525
|
* verdict's capture was clean — see `RingBuffer.lostSince`, and never the raw drop counter, which
|
|
546
|
-
* moves for the age and churn evictions that every live page produces continuously.
|
|
526
|
+
* moves for the age and churn evictions that every live page produces continuously. The journal is
|
|
527
|
+
* the other half of that store, so a durable read that could not reach back to `cursor` lost
|
|
528
|
+
* exactly what an eviction would have. Both boundaries are INCLUSIVE (`t` is a millisecond many
|
|
529
|
+
* records share) and an absent `lostThroughT` impeaches every window rather than none — the
|
|
530
|
+
* conservative direction, both times.
|
|
547
531
|
*/
|
|
548
532
|
lostSince(cursor) {
|
|
549
|
-
|
|
533
|
+
if (this.#buffer.lostSince(cursor))
|
|
534
|
+
return true;
|
|
535
|
+
const lost = this.#journalReader?.readLoss?.();
|
|
536
|
+
return lost !== undefined && (lost.lostThroughT === undefined || lost.lostThroughT >= cursor);
|
|
550
537
|
}
|
|
551
538
|
onEvent(listener) {
|
|
552
539
|
this.#listeners.add(listener);
|
package/dist/status-payload.d.ts
CHANGED
|
@@ -19,12 +19,23 @@ interface StatusPayload {
|
|
|
19
19
|
* beside a live session.
|
|
20
20
|
*/
|
|
21
21
|
why?: string;
|
|
22
|
+
/**
|
|
23
|
+
* The same diagnosis with the differential left off: the lead and the next action.
|
|
24
|
+
*
|
|
25
|
+
* Two fields because there are two readers. `why` is for an agent about to spend a drive on a
|
|
26
|
+
* guess, and carries the ranked causes, the port-scan caveat and the lease offer. `whyLead` is
|
|
27
|
+
* for a surface a PERSON reads: `reticle init` prints the daemon's account of a failed connect,
|
|
28
|
+
* and was printing roughly five hundred words of it in one paragraph.
|
|
29
|
+
*/
|
|
30
|
+
whyLead?: string;
|
|
22
31
|
/** Port of the verify HTTP endpoint this daemon serves — present only when started with `--http`. */
|
|
23
32
|
verifyPort?: number;
|
|
24
33
|
}
|
|
25
34
|
export declare function statusPayload(sessionCount: number, sessions: SessionInfo[],
|
|
26
35
|
/** The no-session diagnosis, injected so this stays pure and the port probe stays off this path. */
|
|
27
|
-
why?: string, verifyPort?: number
|
|
36
|
+
why?: string, verifyPort?: number,
|
|
37
|
+
/** The short rendering of the same diagnosis. Absent on a daemon that has no provider wired. */
|
|
38
|
+
whyLead?: string): StatusPayload;
|
|
28
39
|
/**
|
|
29
40
|
* Why `serve --http` cannot claim success against an already-running daemon — or undefined when
|
|
30
41
|
* that daemon already serves the verify HTTP endpoint on the wanted port.
|