cbrowser 20.2.2 → 21.0.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/README.md +2 -2
- package/dist/analysis/accessibility-empathy.d.ts +29 -1
- package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +69 -35
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +11 -7
- package/dist/browser.js.map +1 -1
- package/dist/cif-score.d.ts +56 -5
- package/dist/cif-score.d.ts.map +1 -1
- package/dist/cif-score.js +595 -346
- package/dist/cif-score.js.map +1 -1
- package/dist/cli.js +10 -8
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/default-traits.d.ts +14 -0
- package/dist/cognitive/default-traits.d.ts.map +1 -0
- package/dist/cognitive/default-traits.js +36 -0
- package/dist/cognitive/default-traits.js.map +1 -0
- package/dist/cognitive/emotions.d.ts.map +1 -1
- package/dist/cognitive/emotions.js +104 -174
- package/dist/cognitive/emotions.js.map +1 -1
- package/dist/cognitive/index.d.ts +28 -0
- package/dist/cognitive/index.d.ts.map +1 -1
- package/dist/cognitive/index.js +353 -287
- package/dist/cognitive/index.js.map +1 -1
- package/dist/cognitive/temporal.d.ts +0 -24
- package/dist/cognitive/temporal.d.ts.map +1 -1
- package/dist/cognitive/temporal.js +45 -35
- package/dist/cognitive/temporal.js.map +1 -1
- package/dist/data/ctc-reference-v2-2026-10-24.d.ts +16 -0
- package/dist/data/ctc-reference-v2-2026-10-24.d.ts.map +1 -0
- package/dist/data/ctc-reference-v2-2026-10-24.js +1004 -0
- package/dist/data/ctc-reference-v2-2026-10-24.js.map +1 -0
- package/dist/ideal-profiles-gap.d.ts +1 -0
- package/dist/ideal-profiles-gap.d.ts.map +1 -1
- package/dist/ideal-profiles-gap.js +9 -1
- package/dist/ideal-profiles-gap.js.map +1 -1
- package/dist/ideal-profiles.d.ts +8 -0
- package/dist/ideal-profiles.d.ts.map +1 -1
- package/dist/ideal-profiles.js +28 -24
- package/dist/ideal-profiles.js.map +1 -1
- package/dist/mcp-server-remote.d.ts.map +1 -1
- package/dist/mcp-server-remote.js +7 -8
- package/dist/mcp-server-remote.js.map +1 -1
- package/dist/mcp-tools/account-personas.d.ts.map +1 -1
- package/dist/mcp-tools/account-personas.js +3 -1
- package/dist/mcp-tools/account-personas.js.map +1 -1
- package/dist/mcp-tools/base/answer-provenance.d.ts +28 -0
- package/dist/mcp-tools/base/answer-provenance.d.ts.map +1 -0
- package/dist/mcp-tools/base/answer-provenance.js +32 -0
- package/dist/mcp-tools/base/answer-provenance.js.map +1 -0
- package/dist/mcp-tools/base/audit-tools.d.ts +0 -3
- package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/audit-tools.js +152 -84
- package/dist/mcp-tools/base/audit-tools.js.map +1 -1
- package/dist/mcp-tools/base/capture-tools.js +1 -1
- package/dist/mcp-tools/base/capture-tools.js.map +1 -1
- package/dist/mcp-tools/base/chain-values.d.ts +38 -0
- package/dist/mcp-tools/base/chain-values.d.ts.map +1 -0
- package/dist/mcp-tools/base/chain-values.js +85 -0
- package/dist/mcp-tools/base/chain-values.js.map +1 -0
- package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.js +126 -120
- package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
- package/dist/mcp-tools/base/gif-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/gif-tools.js +23 -5
- package/dist/mcp-tools/base/gif-tools.js.map +1 -1
- package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/persona-comparison-tools.js +229 -172
- package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
- package/dist/mcp-tools/base/site-familiarity.d.ts +31 -0
- package/dist/mcp-tools/base/site-familiarity.d.ts.map +1 -0
- package/dist/mcp-tools/base/site-familiarity.js +30 -0
- package/dist/mcp-tools/base/site-familiarity.js.map +1 -0
- package/dist/mcp-tools/base/site-knowledge-tools.js +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
- package/dist/mcp-tools/base/story-narrative.d.ts +50 -0
- package/dist/mcp-tools/base/story-narrative.d.ts.map +1 -0
- package/dist/mcp-tools/base/story-narrative.js +88 -0
- package/dist/mcp-tools/base/story-narrative.js.map +1 -0
- package/dist/mcp-tools/base/values-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/values-tools.js +19 -5
- package/dist/mcp-tools/base/values-tools.js.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.d.ts +5 -5
- package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.js +38 -11
- package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
- package/dist/mcp-tools/persona-creation-tools.js +6 -6
- package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
- package/dist/mcp-tools/persona-lifecycle-tools.d.ts.map +1 -1
- package/dist/mcp-tools/persona-lifecycle-tools.js +5 -1
- package/dist/mcp-tools/persona-lifecycle-tools.js.map +1 -1
- package/dist/mcp-tools/tier-gate.js +1 -1
- package/dist/mcp-tools/tool-result-saver.d.ts +7 -0
- package/dist/mcp-tools/tool-result-saver.d.ts.map +1 -1
- package/dist/mcp-tools/tool-result-saver.js +11 -0
- package/dist/mcp-tools/tool-result-saver.js.map +1 -1
- package/dist/mcp-tools/trait-provenance.d.ts +109 -0
- package/dist/mcp-tools/trait-provenance.d.ts.map +1 -0
- package/dist/mcp-tools/trait-provenance.js +187 -0
- package/dist/mcp-tools/trait-provenance.js.map +1 -0
- package/dist/mcp-tools/ui-resources.d.ts +6 -7
- package/dist/mcp-tools/ui-resources.d.ts.map +1 -1
- package/dist/mcp-tools/ui-resources.js +162 -12
- package/dist/mcp-tools/ui-resources.js.map +1 -1
- package/dist/mcp-tools/widget-kit.d.ts +129 -3
- package/dist/mcp-tools/widget-kit.d.ts.map +1 -1
- package/dist/mcp-tools/widget-kit.js +507 -28
- package/dist/mcp-tools/widget-kit.js.map +1 -1
- package/dist/performance/metrics.d.ts.map +1 -1
- package/dist/performance/metrics.js +4 -1
- package/dist/performance/metrics.js.map +1 -1
- package/dist/persona-questionnaire.js +1 -1
- package/dist/persona-questionnaire.js.map +1 -1
- package/dist/personas.d.ts.map +1 -1
- package/dist/personas.js +133 -82
- package/dist/personas.js.map +1 -1
- package/dist/recording/engine.d.ts.map +1 -1
- package/dist/recording/engine.js +5 -2
- package/dist/recording/engine.js.map +1 -1
- package/dist/recording/saliency-overlay.d.ts +8 -3
- package/dist/recording/saliency-overlay.d.ts.map +1 -1
- package/dist/recording/saliency-overlay.js +14 -5
- package/dist/recording/saliency-overlay.js.map +1 -1
- package/dist/recording/types.js +1 -1
- package/dist/testing/nl-test-suite.d.ts.map +1 -1
- package/dist/testing/nl-test-suite.js +2 -1
- package/dist/testing/nl-test-suite.js.map +1 -1
- package/dist/trait-reference.d.ts +19 -2
- package/dist/trait-reference.d.ts.map +1 -1
- package/dist/trait-reference.js +19 -2
- package/dist/trait-reference.js.map +1 -1
- package/dist/types.d.ts +32 -21
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +62 -52
- package/dist/types.js.map +1 -1
- package/dist/values/persona-completeness.js +1 -1
- package/dist/values/persona-completeness.js.map +1 -1
- package/dist/visual/ab-comparison.d.ts.map +1 -1
- package/dist/visual/ab-comparison.js +4 -1
- package/dist/visual/ab-comparison.js.map +1 -1
- package/dist/visual/attention-quality.d.ts +1 -1
- package/dist/visual/attention-quality.js +1 -1
- package/dist/visual/attention-transport.d.ts +74 -10
- package/dist/visual/attention-transport.d.ts.map +1 -1
- package/dist/visual/attention-transport.js +353 -193
- package/dist/visual/attention-transport.js.map +1 -1
- package/dist/visual/cognitive-models.d.ts +77 -55
- package/dist/visual/cognitive-models.d.ts.map +1 -1
- package/dist/visual/cognitive-models.js +231 -104
- package/dist/visual/cognitive-models.js.map +1 -1
- package/dist/visual/cognitive-transport-chain.d.ts +328 -30
- package/dist/visual/cognitive-transport-chain.d.ts.map +1 -1
- package/dist/visual/cognitive-transport-chain.js +576 -353
- package/dist/visual/cognitive-transport-chain.js.map +1 -1
- package/dist/visual/cognitive-transport.d.ts +446 -13
- package/dist/visual/cognitive-transport.d.ts.map +1 -1
- package/dist/visual/cognitive-transport.js +1085 -245
- package/dist/visual/cognitive-transport.js.map +1 -1
- package/dist/visual/cross-browser.d.ts.map +1 -1
- package/dist/visual/cross-browser.js +8 -6
- package/dist/visual/cross-browser.js.map +1 -1
- package/dist/visual/ctc-bands.d.ts +91 -0
- package/dist/visual/ctc-bands.d.ts.map +1 -0
- package/dist/visual/ctc-bands.js +117 -0
- package/dist/visual/ctc-bands.js.map +1 -0
- package/dist/visual/ctc-reference.d.ts +287 -0
- package/dist/visual/ctc-reference.d.ts.map +1 -0
- package/dist/visual/ctc-reference.js +376 -0
- package/dist/visual/ctc-reference.js.map +1 -0
- package/dist/visual/demand-laws.d.ts +145 -0
- package/dist/visual/demand-laws.d.ts.map +1 -0
- package/dist/visual/demand-laws.js +220 -0
- package/dist/visual/demand-laws.js.map +1 -0
- package/dist/visual/distance-metrics.d.ts +23 -1
- package/dist/visual/distance-metrics.d.ts.map +1 -1
- package/dist/visual/distance-metrics.js +49 -18
- package/dist/visual/distance-metrics.js.map +1 -1
- package/dist/visual/dro-bound.d.ts +220 -0
- package/dist/visual/dro-bound.d.ts.map +1 -0
- package/dist/visual/dro-bound.js +341 -0
- package/dist/visual/dro-bound.js.map +1 -0
- package/dist/visual/exact-w1.d.ts +60 -0
- package/dist/visual/exact-w1.d.ts.map +1 -0
- package/dist/visual/exact-w1.js +247 -0
- package/dist/visual/exact-w1.js.map +1 -0
- package/dist/visual/human-input.d.ts.map +1 -1
- package/dist/visual/human-input.js +45 -41
- package/dist/visual/human-input.js.map +1 -1
- package/dist/visual/index.d.ts +2 -0
- package/dist/visual/index.d.ts.map +1 -1
- package/dist/visual/index.js +2 -0
- package/dist/visual/index.js.map +1 -1
- package/dist/visual/law-times-report.d.ts +49 -0
- package/dist/visual/law-times-report.d.ts.map +1 -0
- package/dist/visual/law-times-report.js +38 -0
- package/dist/visual/law-times-report.js.map +1 -0
- package/dist/visual/model-provenance.d.ts +151 -0
- package/dist/visual/model-provenance.d.ts.map +1 -0
- package/dist/visual/model-provenance.js +334 -0
- package/dist/visual/model-provenance.js.map +1 -0
- package/dist/visual/normal-cdf.d.ts +14 -0
- package/dist/visual/normal-cdf.d.ts.map +1 -0
- package/dist/visual/normal-cdf.js +26 -0
- package/dist/visual/normal-cdf.js.map +1 -0
- package/dist/visual/perceptual-transport.d.ts +28 -4
- package/dist/visual/perceptual-transport.d.ts.map +1 -1
- package/dist/visual/perceptual-transport.js +66 -32
- package/dist/visual/perceptual-transport.js.map +1 -1
- package/dist/visual/provenance/attention.d.ts +55 -0
- package/dist/visual/provenance/attention.d.ts.map +1 -0
- package/dist/visual/provenance/attention.js +188 -0
- package/dist/visual/provenance/attention.js.map +1 -0
- package/dist/visual/provenance/chain.d.ts +41 -0
- package/dist/visual/provenance/chain.d.ts.map +1 -0
- package/dist/visual/provenance/chain.js +66 -0
- package/dist/visual/provenance/chain.js.map +1 -0
- package/dist/visual/provenance/cif.d.ts +42 -0
- package/dist/visual/provenance/cif.d.ts.map +1 -0
- package/dist/visual/provenance/cif.js +116 -0
- package/dist/visual/provenance/cif.js.map +1 -0
- package/dist/visual/provenance/ctc-reference.d.ts +36 -0
- package/dist/visual/provenance/ctc-reference.d.ts.map +1 -0
- package/dist/visual/provenance/ctc-reference.js +96 -0
- package/dist/visual/provenance/ctc-reference.js.map +1 -0
- package/dist/visual/provenance/distance.d.ts +10 -0
- package/dist/visual/provenance/distance.d.ts.map +1 -0
- package/dist/visual/provenance/distance.js +72 -0
- package/dist/visual/provenance/distance.js.map +1 -0
- package/dist/visual/provenance/dro.d.ts +10 -0
- package/dist/visual/provenance/dro.d.ts.map +1 -0
- package/dist/visual/provenance/dro.js +2 -0
- package/dist/visual/provenance/dro.js.map +1 -0
- package/dist/visual/provenance/emotion.d.ts +26 -0
- package/dist/visual/provenance/emotion.d.ts.map +1 -0
- package/dist/visual/provenance/emotion.js +99 -0
- package/dist/visual/provenance/emotion.js.map +1 -0
- package/dist/visual/provenance/human-input.d.ts +19 -0
- package/dist/visual/provenance/human-input.d.ts.map +1 -0
- package/dist/visual/provenance/human-input.js +84 -0
- package/dist/visual/provenance/human-input.js.map +1 -0
- package/dist/visual/provenance/journey-group.d.ts +15 -0
- package/dist/visual/provenance/journey-group.d.ts.map +1 -0
- package/dist/visual/provenance/journey-group.js +15 -0
- package/dist/visual/provenance/journey-group.js.map +1 -0
- package/dist/visual/provenance/journey-mcp.d.ts +24 -0
- package/dist/visual/provenance/journey-mcp.d.ts.map +1 -0
- package/dist/visual/provenance/journey-mcp.js +54 -0
- package/dist/visual/provenance/journey-mcp.js.map +1 -0
- package/dist/visual/provenance/journey.d.ts +34 -0
- package/dist/visual/provenance/journey.d.ts.map +1 -0
- package/dist/visual/provenance/journey.js +184 -0
- package/dist/visual/provenance/journey.js.map +1 -0
- package/dist/visual/provenance/laws.d.ts +38 -0
- package/dist/visual/provenance/laws.d.ts.map +1 -0
- package/dist/visual/provenance/laws.js +40 -0
- package/dist/visual/provenance/laws.js.map +1 -0
- package/dist/visual/provenance/page-metrics.d.ts +42 -0
- package/dist/visual/provenance/page-metrics.d.ts.map +1 -0
- package/dist/visual/provenance/page-metrics.js +79 -0
- package/dist/visual/provenance/page-metrics.js.map +1 -0
- package/dist/visual/provenance/perceptual.d.ts +63 -0
- package/dist/visual/provenance/perceptual.d.ts.map +1 -0
- package/dist/visual/provenance/perceptual.js +115 -0
- package/dist/visual/provenance/perceptual.js.map +1 -0
- package/dist/visual/provenance/persona-rates.d.ts +84 -0
- package/dist/visual/provenance/persona-rates.d.ts.map +1 -0
- package/dist/visual/provenance/persona-rates.js +112 -0
- package/dist/visual/provenance/persona-rates.js.map +1 -0
- package/dist/visual/provenance/personas.d.ts +122 -0
- package/dist/visual/provenance/personas.d.ts.map +1 -0
- package/dist/visual/provenance/personas.js +298 -0
- package/dist/visual/provenance/personas.js.map +1 -0
- package/dist/visual/provenance/profiles.d.ts +39 -0
- package/dist/visual/provenance/profiles.d.ts.map +1 -0
- package/dist/visual/provenance/profiles.js +37 -0
- package/dist/visual/provenance/profiles.js.map +1 -0
- package/dist/visual/provenance/temporal.d.ts +21 -0
- package/dist/visual/provenance/temporal.d.ts.map +1 -0
- package/dist/visual/provenance/temporal.js +56 -0
- package/dist/visual/provenance/temporal.js.map +1 -0
- package/dist/visual/provenance/visual-distance.d.ts +22 -0
- package/dist/visual/provenance/visual-distance.d.ts.map +1 -0
- package/dist/visual/provenance/visual-distance.js +30 -0
- package/dist/visual/provenance/visual-distance.js.map +1 -0
- package/dist/visual/regression.d.ts.map +1 -1
- package/dist/visual/regression.js +14 -9
- package/dist/visual/regression.js.map +1 -1
- package/dist/visual/responsive.d.ts.map +1 -1
- package/dist/visual/responsive.js +4 -1
- package/dist/visual/responsive.js.map +1 -1
- package/dist/visual/scope-prose.js +2 -2
- package/dist/visual/scope-prose.js.map +1 -1
- package/dist/visual/seeded-random.d.ts +16 -0
- package/dist/visual/seeded-random.d.ts.map +1 -0
- package/dist/visual/seeded-random.js +25 -0
- package/dist/visual/seeded-random.js.map +1 -0
- package/dist/visual/stored-accessibility-traits.d.ts +27 -0
- package/dist/visual/stored-accessibility-traits.d.ts.map +1 -0
- package/dist/visual/stored-accessibility-traits.js +35 -0
- package/dist/visual/stored-accessibility-traits.js.map +1 -0
- package/package.json +1 -1
package/dist/cognitive/index.js
CHANGED
|
@@ -31,7 +31,12 @@ import { isAgentPersonaObject } from "../agent-personas.js";
|
|
|
31
31
|
import { createInitialEmotionalState, createEmotionalConfig, applyEmotionalTrigger, decayEmotionsWithTraits, deriveSimplifiedState, calculateAbandonmentModifier, describeEmotionalState, shouldConsiderAbandonment, } from "./emotions.js";
|
|
32
32
|
import { calculateFatigueIncrement, calculateFittsMovementTime, shouldSwitchToSystem2, canReturnToSystem1, calculateTypingTime, calculateScanWidthMultiplier, calculateGazeMouseLag, calculatePeripheralVision, createHabituationState, updateHabituationState, classifyUIPattern, } from "../types.js";
|
|
33
33
|
import { loadConfigFile, getDataDir } from "../config.js";
|
|
34
|
+
import { param } from "../visual/model-provenance.js";
|
|
35
|
+
import { defaultJourneyTraits } from "./default-traits.js";
|
|
36
|
+
import { distanceToScreenCentre, smallerOfWidth } from "../visual/demand-laws.js";
|
|
34
37
|
import { writeSecretFile } from "../secure-file.js";
|
|
38
|
+
import { uniqueStamp } from "../temp-files.js";
|
|
39
|
+
import { semanticWeightTable } from "../visual/attention-transport.js";
|
|
35
40
|
import { JourneyTraceWriter, TraceReplayProvider, hashPageState, } from "./journey-trace.js";
|
|
36
41
|
// ============================================================================
|
|
37
42
|
// API Key Management
|
|
@@ -421,7 +426,12 @@ const MOUSE_OVERLAY_SCRIPT = `(() => {
|
|
|
421
426
|
* payload, no dashboard-side overlay positioning. Per-frame compute is
|
|
422
427
|
* sub-millisecond on modern pages because we're just reading bounding boxes.
|
|
423
428
|
*
|
|
424
|
-
* Config is injected as `window.__cbAttentionConfig` by the journey runner
|
|
429
|
+
* Config is injected as `window.__cbAttentionConfig` by the journey runner
|
|
430
|
+
* (attentionOverlayInitScript). Every weight and goal constant comes from it,
|
|
431
|
+
* that is from attention-transport's semanticWeightTable; this script holds
|
|
432
|
+
* none of its own (ISC-48.1, 2026-10-22). It is still a simpler model than
|
|
433
|
+
* analyzeAttention: max per cell, max-normalised, no values layer, no
|
|
434
|
+
* perceptual filter, no gaze calibration, and its own element classifier.
|
|
425
435
|
*/
|
|
426
436
|
const ATTENTION_OVERLAY_SCRIPT = `(() => {
|
|
427
437
|
if (window.__cbAttentionInstalled || window.__cbAttentionPending) return;
|
|
@@ -430,9 +440,11 @@ const ATTENTION_OVERLAY_SCRIPT = `(() => {
|
|
|
430
440
|
const install = () => {
|
|
431
441
|
if (window.__cbAttentionInstalled) return;
|
|
432
442
|
if (!document.documentElement || !document.body) return;
|
|
443
|
+
// No config, no overlay: drawing with invented weights is what this
|
|
444
|
+
// replaced.
|
|
445
|
+
const config = window.__cbAttentionConfig;
|
|
446
|
+
if (!config) return;
|
|
433
447
|
window.__cbAttentionInstalled = true;
|
|
434
|
-
|
|
435
|
-
const config = window.__cbAttentionConfig || { typeWeights: {}, personaMods: {}, goal: "" };
|
|
436
448
|
const cellSize = 16;
|
|
437
449
|
|
|
438
450
|
const css = \`
|
|
@@ -514,15 +526,16 @@ const ATTENTION_OVERLAY_SCRIPT = `(() => {
|
|
|
514
526
|
return 'content';
|
|
515
527
|
};
|
|
516
528
|
|
|
517
|
-
//
|
|
529
|
+
// Goal relevance as computeGoalRelevance scores it, with its constants.
|
|
530
|
+
const stopWords = new Set(config.stopWords || []);
|
|
518
531
|
const goalWords = (config.goal || '').toLowerCase()
|
|
519
|
-
.split(/\\s+/).filter(w => w.length
|
|
532
|
+
.split(/\\s+/).filter(w => w.length >= config.minKeywordLength && !stopWords.has(w));
|
|
520
533
|
const goalRelevance = (text) => {
|
|
521
534
|
if (!text || !goalWords.length) return 0;
|
|
522
535
|
const t = text.toLowerCase();
|
|
523
536
|
let m = 0;
|
|
524
537
|
for (const w of goalWords) if (t.includes(w)) m++;
|
|
525
|
-
return (m / goalWords.length) *
|
|
538
|
+
return (m / goalWords.length) * config.relevanceGain;
|
|
526
539
|
};
|
|
527
540
|
|
|
528
541
|
let lastCells = null;
|
|
@@ -553,9 +566,10 @@ const ATTENTION_OVERLAY_SCRIPT = `(() => {
|
|
|
553
566
|
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') continue;
|
|
554
567
|
|
|
555
568
|
const type = classify(el);
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
weight
|
|
569
|
+
// As buildSemanticMap: an unknown type takes the fallback weight, and
|
|
570
|
+
// a type without a modifier is unmodified.
|
|
571
|
+
let weight = config.typeWeights[type] ?? config.typeFallback;
|
|
572
|
+
weight *= config.personaMods[type] ?? 1;
|
|
559
573
|
const text = (el.innerText || el.textContent || '').slice(0, 200);
|
|
560
574
|
const gr = goalRelevance(text);
|
|
561
575
|
if (gr > 0) weight *= (1 + gr);
|
|
@@ -621,6 +635,11 @@ const ATTENTION_OVERLAY_SCRIPT = `(() => {
|
|
|
621
635
|
canvas.style.display = visible === false ? 'none' : '';
|
|
622
636
|
};
|
|
623
637
|
window.__cbAttentionRefresh = schedule;
|
|
638
|
+
// The raw weights of the last frame (before max-normalisation), so a test
|
|
639
|
+
// can read what was painted rather than decode the canvas.
|
|
640
|
+
window.__cbAttentionCells = () => lastCells
|
|
641
|
+
? { cols: lastDims.cols, rows: lastDims.rows, cellSize, cells: Array.from(lastCells) }
|
|
642
|
+
: null;
|
|
624
643
|
};
|
|
625
644
|
|
|
626
645
|
if (typeof window.requestIdleCallback === 'function') {
|
|
@@ -632,32 +651,26 @@ const ATTENTION_OVERLAY_SCRIPT = `(() => {
|
|
|
632
651
|
/**
|
|
633
652
|
* Build the JSON config bundle that drives the in-browser attention heatmap.
|
|
634
653
|
* The browser-side script doesn't have access to the SDK's persona profiles,
|
|
635
|
-
* so
|
|
654
|
+
* so the weight table is computed here and shipped.
|
|
655
|
+
*
|
|
656
|
+
* The table is attention-transport's own (semanticWeightTable), not a copy.
|
|
657
|
+
* Until 2026-10-22 this function held a hand-written mirror "kept in sync by
|
|
658
|
+
* hand", and it had drifted: navigation still read siteFamiliarity, which the
|
|
659
|
+
* model dropped on 2026-08-05, so a journey given customTraits
|
|
660
|
+
* { siteFamiliarity: 0 } drew navigation at 1.0 while attention_analysis drew
|
|
661
|
+
* it at 0.7. The page script also kept its own fallback weight, goal gain,
|
|
662
|
+
* keyword length and stop words; they now come from here too. (ISC-48.1)
|
|
636
663
|
*/
|
|
637
|
-
function buildAttentionConfig(personaName, goal, traits) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
image: 0.5, navigation: 0.4, link: 0.3,
|
|
643
|
-
content: 0.2, decorative: 0.1, price: 0.9, error: 0.9,
|
|
644
|
-
};
|
|
645
|
-
// Trait-driven modifiers. Mirrors computeTraitModifiers in attention-transport.
|
|
646
|
-
const t = (k) => traits?.[k] ?? 0.5;
|
|
647
|
-
const personaMods = {
|
|
648
|
-
cta: 1.0 + (1.0 - t("patience")) * 0.5,
|
|
649
|
-
search: 0.7 + (1.0 - t("patience")) * 0.6,
|
|
650
|
-
content: t("readingTendency") > 0.6 ? 0.4 + t("readingTendency") * 0.4 : 0.2 + t("patience") * 0.4,
|
|
651
|
-
heading: 0.8 + t("curiosity") * 0.4,
|
|
652
|
-
image: 0.5 + t("curiosity") * 0.5,
|
|
653
|
-
navigation: 0.4 + (1.0 - t("siteFamiliarity")) * 0.6,
|
|
654
|
-
error: 0.5 + (1.0 - t("selfEfficacy")) * 0.5,
|
|
655
|
-
price: 0.5 + (1.0 - t("riskTolerance")) * 0.5,
|
|
656
|
-
decorative: 0.1 + (1.0 - t("metacognitivePlanning")) * 0.4,
|
|
657
|
-
form: 0.5 + t("proceduralFluency") * 0.3,
|
|
658
|
-
link: 1.0,
|
|
664
|
+
export function buildAttentionConfig(personaName, goal, traits) {
|
|
665
|
+
return {
|
|
666
|
+
...semanticWeightTable(personaName, traits),
|
|
667
|
+
goal: goal || "",
|
|
668
|
+
persona: personaName,
|
|
659
669
|
};
|
|
660
|
-
|
|
670
|
+
}
|
|
671
|
+
/** The init script that installs the in-page heatmap with `config`. */
|
|
672
|
+
export function attentionOverlayInitScript(config) {
|
|
673
|
+
return `window.__cbAttentionConfig = ${JSON.stringify(config)};` + ATTENTION_OVERLAY_SCRIPT;
|
|
661
674
|
}
|
|
662
675
|
// Note: CognitiveJourneyOptions accepts `device` to emulate a specific device
|
|
663
676
|
// preset (mobile/tablet/desktop or "iPhone 15", etc.). Wired in below alongside
|
|
@@ -702,7 +715,7 @@ export async function runCognitiveJourney(options) {
|
|
|
702
715
|
(options.replayTrace ? new TraceReplayProvider(options.replayTrace) : liveLLMProvider);
|
|
703
716
|
const nameSlug = options.name ? journeySlug(options.name) : undefined;
|
|
704
717
|
const traceWriter = options.trace
|
|
705
|
-
? new JourneyTraceWriter(nameSlug ? { journeyId: `journey-${nameSlug}-${
|
|
718
|
+
? new JourneyTraceWriter(nameSlug ? { journeyId: `journey-${nameSlug}-${uniqueStamp()}` } : undefined)
|
|
706
719
|
: null;
|
|
707
720
|
const personaObj = resolveJourneyPersona(options.persona, options.customTraits);
|
|
708
721
|
const profile = getCognitiveProfile(personaObj);
|
|
@@ -715,26 +728,31 @@ export async function runCognitiveJourney(options) {
|
|
|
715
728
|
// (low-vision-magnified, motor-impairment-tremor) get extra penalty so
|
|
716
729
|
// their clicks visibly miss more often.
|
|
717
730
|
const aTraits = personaObj.accessibilityTraits;
|
|
718
|
-
const motorBase = traits.proceduralFluency ??
|
|
731
|
+
const motorBase = traits.proceduralFluency ?? param("journey.defaultTrait");
|
|
719
732
|
const motorPenalty = aTraits?.motorControl !== undefined ? aTraits.motorControl : 1.0;
|
|
720
733
|
const inputTraits = {
|
|
721
|
-
proceduralFluency: traits.proceduralFluency ??
|
|
722
|
-
motorPrecision: Math.max(
|
|
723
|
-
patience: traits.patience ??
|
|
724
|
-
readingTendency: traits.readingTendency ??
|
|
734
|
+
proceduralFluency: traits.proceduralFluency ?? param("journey.defaultTrait"),
|
|
735
|
+
motorPrecision: Math.max(param("journey.input.motorPrecisionFloor"), Math.min(1, motorBase * motorPenalty)),
|
|
736
|
+
patience: traits.patience ?? param("journey.defaultTrait"),
|
|
737
|
+
readingTendency: traits.readingTendency ?? param("journey.defaultTrait"),
|
|
725
738
|
};
|
|
726
739
|
// Calculate abandonment thresholds
|
|
740
|
+
// Every limit is registered (journey.abandon.*, src/visual/provenance/journey.ts).
|
|
741
|
+
const lowComprehension = traits.comprehension < param("journey.band.low.comprehension");
|
|
742
|
+
const lowPatience = traits.patience < param("journey.band.low.patience");
|
|
743
|
+
const highPatience = traits.patience > param("journey.band.high.patience");
|
|
744
|
+
const lowPersistence = traits.persistence < param("journey.band.low.persistence");
|
|
745
|
+
const highPersistence = traits.persistence > param("journey.band.high.persistence");
|
|
727
746
|
const thresholds = {
|
|
728
|
-
patienceMin:
|
|
729
|
-
confusionMax:
|
|
730
|
-
frustrationMax:
|
|
731
|
-
maxStepsWithoutProgress:
|
|
732
|
-
loopDetectionThreshold: 3,
|
|
747
|
+
patienceMin: param("journey.abandon.patienceFloor"),
|
|
748
|
+
confusionMax: lowComprehension ? param("journey.abandon.confusionLimit.lowComprehension") : param("journey.abandon.confusionLimit"),
|
|
749
|
+
frustrationMax: lowPatience ? param("journey.abandon.frustrationLimit.lowPatience") : param("journey.abandon.frustrationLimit"),
|
|
750
|
+
maxStepsWithoutProgress: highPersistence ? param("journey.abandon.noProgressSteps.highPersistence") : param("journey.abandon.noProgressSteps"),
|
|
733
751
|
timeLimit: options.maxTime ||
|
|
734
|
-
(
|
|
752
|
+
(highPatience ? param("journey.abandon.timeLimitSeconds.highPatience") : lowPatience ? param("journey.abandon.timeLimitSeconds.lowPatience") : param("journey.abandon.timeLimitSeconds")),
|
|
735
753
|
// Decision fatigue threshold based on persistence (v9.9.0)
|
|
736
754
|
// High persistence = can handle more decisions before giving up
|
|
737
|
-
decisionFatigueMax:
|
|
755
|
+
decisionFatigueMax: highPersistence ? param("journey.abandon.fatigueLimit.highPersistence") : lowPersistence ? param("journey.abandon.fatigueLimit.lowPersistence") : param("journey.abandon.fatigueLimit"),
|
|
738
756
|
};
|
|
739
757
|
// Initialize state
|
|
740
758
|
const state = {
|
|
@@ -742,7 +760,7 @@ export async function runCognitiveJourney(options) {
|
|
|
742
760
|
confusionLevel: 0.0,
|
|
743
761
|
frustrationLevel: 0.0,
|
|
744
762
|
goalProgress: 0.0,
|
|
745
|
-
confidenceLevel:
|
|
763
|
+
confidenceLevel: param("journey.initialConfidence"),
|
|
746
764
|
currentMood: "neutral",
|
|
747
765
|
memory: {
|
|
748
766
|
pagesVisited: [options.startUrl],
|
|
@@ -762,8 +780,8 @@ export async function runCognitiveJourney(options) {
|
|
|
762
780
|
// Dual-process cognitive mode (v10.0.0)
|
|
763
781
|
// Experts start in System 1 (fast), novices in System 2 (slow)
|
|
764
782
|
cognitiveMode: {
|
|
765
|
-
system: traits.comprehension >
|
|
766
|
-
switchThreshold:
|
|
783
|
+
system: traits.comprehension > param("journey.band.high.comprehension") ? 1 : 2,
|
|
784
|
+
switchThreshold: lowComprehension ? param("journey.dualProcess.switchThreshold.lowComprehension") : param("journey.dualProcess.switchThreshold"),
|
|
767
785
|
system1Errors: 0,
|
|
768
786
|
timeInSystem1: 0,
|
|
769
787
|
timeInSystem2: 0,
|
|
@@ -772,7 +790,7 @@ export async function runCognitiveJourney(options) {
|
|
|
772
790
|
scanPattern: {
|
|
773
791
|
basePattern: personaObj.humanBehavior?.attention?.pattern || "f-pattern",
|
|
774
792
|
widthMultiplier: 1.0,
|
|
775
|
-
effectiveWidth:
|
|
793
|
+
effectiveWidth: param("journey.percentScale"),
|
|
776
794
|
},
|
|
777
795
|
missedDueToTunnelVision: 0,
|
|
778
796
|
// Peripheral vision affected by stress (v10.1.0) - Yerkes-Dodson Law
|
|
@@ -784,7 +802,9 @@ export async function runCognitiveJourney(options) {
|
|
|
784
802
|
// Habituation/banner blindness state (v10.1.0)
|
|
785
803
|
habituation: createHabituationState(
|
|
786
804
|
// Low comprehension = faster blindness (lower threshold)
|
|
787
|
-
traits.comprehension >
|
|
805
|
+
traits.comprehension > param("journey.band.high.comprehension")
|
|
806
|
+
? param("journey.habituation.exposures.highComprehension")
|
|
807
|
+
: lowComprehension ? param("journey.habituation.exposures.lowComprehension") : param("journey.habituation.exposures")),
|
|
788
808
|
// Gaze-to-mouse lag based on age (v10.1.0) - WebGazer.js research
|
|
789
809
|
gazeMouseLag: calculateGazeMouseLag(personaObj.demographics?.age_range),
|
|
790
810
|
// Emotional state based on appraisal theory (v13.1.0) - Scherer 2001
|
|
@@ -805,14 +825,15 @@ export async function runCognitiveJourney(options) {
|
|
|
805
825
|
if (ageMatch) {
|
|
806
826
|
const age = parseInt(ageMatch[1], 10);
|
|
807
827
|
// Motor control degrades with age: 1.0 at 25, 1.5 at 65, 2.0 at 85
|
|
808
|
-
|
|
828
|
+
const onset = param("journey.fitts.ageOnset");
|
|
829
|
+
ageModifier = age > onset ? 1 + (age - onset) / param("journey.fitts.agePerSlowdown") : 1.0;
|
|
809
830
|
}
|
|
810
831
|
}
|
|
811
832
|
// Derive tremor modifier from mouse params or motor traits
|
|
812
833
|
let tremorModifier = 0;
|
|
813
834
|
if (mouseParams) {
|
|
814
835
|
// Higher jitter = more tremor effect
|
|
815
|
-
tremorModifier = (mouseParams.jitter || 0) /
|
|
836
|
+
tremorModifier = (mouseParams.jitter || 0) / param("journey.fitts.jitterScale");
|
|
816
837
|
}
|
|
817
838
|
const fittsParams = {
|
|
818
839
|
ageModifier,
|
|
@@ -848,14 +869,14 @@ export async function runCognitiveJourney(options) {
|
|
|
848
869
|
// Window size stays at 1920×1080 so chromium fills Xvfb edge-to-edge.
|
|
849
870
|
// Dashboard overlay positions: top:7.87% (85/1080), height:92.13% (995/1080).
|
|
850
871
|
if (options.enableMouseOverlay) {
|
|
851
|
-
browserConfig.viewportWidth =
|
|
852
|
-
browserConfig.viewportHeight =
|
|
853
|
-
browserConfig.windowWidth =
|
|
854
|
-
browserConfig.windowHeight =
|
|
872
|
+
browserConfig.viewportWidth = param("journey.live.screenWidth");
|
|
873
|
+
browserConfig.viewportHeight = param("journey.live.pageHeight");
|
|
874
|
+
browserConfig.windowWidth = param("journey.live.screenWidth");
|
|
875
|
+
browserConfig.windowHeight = param("journey.live.screenHeight");
|
|
855
876
|
// Heavy university / news / e-commerce sites often need >30s to reach
|
|
856
877
|
// domcontentloaded with all their analytics/CDN scripts. Live journeys
|
|
857
878
|
// get a 60s navigation budget per attempt.
|
|
858
|
-
browserConfig.timeout =
|
|
879
|
+
browserConfig.timeout = param("journey.live.navigationTimeoutMs");
|
|
859
880
|
// When a click opens target="_blank" or window.open, follow the new tab
|
|
860
881
|
// so VNC keeps showing where the persona actually went.
|
|
861
882
|
browserConfig.followPopups = true;
|
|
@@ -865,7 +886,7 @@ export async function runCognitiveJourney(options) {
|
|
|
865
886
|
browserConfig.viewportHeight = options.viewport.height;
|
|
866
887
|
browserConfig.viewportOverride = options.viewport;
|
|
867
888
|
}
|
|
868
|
-
const browser = new CBrowser(browserConfig);
|
|
889
|
+
const browser = options.createBrowser ? options.createBrowser(browserConfig) : new CBrowser(browserConfig);
|
|
869
890
|
// Helper that re-injects the mouse overlay. The overlay script is idempotent
|
|
870
891
|
// (gates on window.__cbHelperInstalled), so calling this after every navigation
|
|
871
892
|
// is safe and ensures the overlay always survives full page loads.
|
|
@@ -892,8 +913,7 @@ export async function runCognitiveJourney(options) {
|
|
|
892
913
|
// Persona modifiers (type weights × cognitive traits) are precomputed
|
|
893
914
|
// here in Node and serialized into the page as window.__cbAttentionConfig.
|
|
894
915
|
const attentionConfig = buildAttentionConfig(personaObj.name, options.goal, traits);
|
|
895
|
-
|
|
896
|
-
await initialPage.context().addInitScript(configBootstrap + ATTENTION_OVERLAY_SCRIPT);
|
|
916
|
+
await initialPage.context().addInitScript(attentionOverlayInitScript(attentionConfig));
|
|
897
917
|
}
|
|
898
918
|
catch (e) {
|
|
899
919
|
console.warn(`[cognitive] Overlay context init failed: ${e.message}`);
|
|
@@ -908,7 +928,8 @@ export async function runCognitiveJourney(options) {
|
|
|
908
928
|
// Hard timeout per attempt is browserConfig.timeout (set to 60s above for
|
|
909
929
|
// live mode).
|
|
910
930
|
let lastErr;
|
|
911
|
-
|
|
931
|
+
const navAttempts = param("journey.nav.attempts");
|
|
932
|
+
for (let attempt = 1; attempt <= navAttempts; attempt++) {
|
|
912
933
|
try {
|
|
913
934
|
await browser.navigate(options.startUrl, { waitStrategy: "domcontentloaded" });
|
|
914
935
|
lastErr = undefined;
|
|
@@ -917,11 +938,11 @@ export async function runCognitiveJourney(options) {
|
|
|
917
938
|
catch (navErr) {
|
|
918
939
|
lastErr = navErr;
|
|
919
940
|
const isTransient = /ERR_NETWORK_CHANGED|ERR_CONNECTION|ERR_TIMED_OUT|Timeout/i.test(lastErr.message);
|
|
920
|
-
if (attempt ===
|
|
941
|
+
if (attempt === navAttempts || !isTransient) {
|
|
921
942
|
if (!isTransient)
|
|
922
943
|
throw lastErr;
|
|
923
944
|
}
|
|
924
|
-
await new Promise(r => setTimeout(r,
|
|
945
|
+
await new Promise(r => setTimeout(r, param("journey.nav.backoffMs") * attempt));
|
|
925
946
|
}
|
|
926
947
|
}
|
|
927
948
|
if (lastErr)
|
|
@@ -931,7 +952,7 @@ export async function runCognitiveJourney(options) {
|
|
|
931
952
|
// Auto-capture (v18.70.0): recording starts once the journey's start URL is
|
|
932
953
|
// loaded, so frame 0 is the page the persona actually opens on, and stops
|
|
933
954
|
// just before the browser closes. Capture never fails the journey.
|
|
934
|
-
const captureSession = await startAutoCapture(await browser.getPage(), options.capture, `journey-${nameSlug ?? journeySlug(options.persona)}-${
|
|
955
|
+
const captureSession = await startAutoCapture(await browser.getPage(), options.capture, `journey-${nameSlug ?? journeySlug(options.persona)}-${uniqueStamp()}`);
|
|
935
956
|
// Apply geolocation at runtime as well (ensures permission is granted after context exists)
|
|
936
957
|
if (effectiveLocation.geolocation) {
|
|
937
958
|
await browser.setGeolocationRuntime(effectiveLocation.geolocation);
|
|
@@ -939,7 +960,7 @@ export async function runCognitiveJourney(options) {
|
|
|
939
960
|
const fullMonologue = [];
|
|
940
961
|
const frictionPoints = [];
|
|
941
962
|
const startTime = Date.now();
|
|
942
|
-
const maxSteps = options.maxSteps ||
|
|
963
|
+
const maxSteps = options.maxSteps || param("journey.maxSteps");
|
|
943
964
|
// ── Returning-visit familiarity boost ─────────────────────────────────
|
|
944
965
|
// The persona's existing siteFamiliarity trait already drives perception
|
|
945
966
|
// (navigation weighting in attention-transport, scan-path defaults, etc.).
|
|
@@ -950,8 +971,8 @@ export async function runCognitiveJourney(options) {
|
|
|
950
971
|
// 5 ≈ +0.72, 10+ ≈ +0.92 — asymptotes near 1.
|
|
951
972
|
if (options.priorMemory && options.priorMemory.visitCount > 0) {
|
|
952
973
|
const v = options.priorMemory.visitCount;
|
|
953
|
-
const familiarityBoost = 1 - Math.exp(-v /
|
|
954
|
-
const baseline = traits.siteFamiliarity ??
|
|
974
|
+
const familiarityBoost = 1 - Math.exp(-v / param("journey.familiarity.visitScale"));
|
|
975
|
+
const baseline = traits.siteFamiliarity ?? param("journey.defaultTrait");
|
|
955
976
|
// Boost toward 1.0 from baseline. The persona keeps any fundamental
|
|
956
977
|
// cognitive trait (e.g. low transferLearning still matters) but the
|
|
957
978
|
// domain-specific familiarity component lifts.
|
|
@@ -973,7 +994,7 @@ export async function runCognitiveJourney(options) {
|
|
|
973
994
|
temporalState = getTemporalState(tCtx);
|
|
974
995
|
temporalModulators = computeTemporalModulators(temporalState);
|
|
975
996
|
// Apply trait deltas
|
|
976
|
-
traits.patience = Math.max(0, Math.min(1, (traits.patience ??
|
|
997
|
+
traits.patience = Math.max(0, Math.min(1, (traits.patience ?? param("journey.defaultTrait")) + temporalModulators.patienceDelta));
|
|
977
998
|
if (traits.workingMemory !== undefined) {
|
|
978
999
|
traits.workingMemory = Math.max(0, Math.min(1, traits.workingMemory + temporalModulators.workingMemoryDelta));
|
|
979
1000
|
}
|
|
@@ -998,7 +1019,7 @@ export async function runCognitiveJourney(options) {
|
|
|
998
1019
|
if (options.priorMemory && options.priorMemory.visitCount > 0) {
|
|
999
1020
|
const m = options.priorMemory;
|
|
1000
1021
|
const sinceDays = m.lastVisitAt
|
|
1001
|
-
? Math.round((Date.now() - new Date(m.lastVisitAt.replace(" ", "T") + "Z").getTime()) / (
|
|
1022
|
+
? Math.round((Date.now() - new Date(m.lastVisitAt.replace(" ", "T") + "Z").getTime()) / param("journey.msPerDay"))
|
|
1002
1023
|
: null;
|
|
1003
1024
|
const sinceStr = sinceDays === null
|
|
1004
1025
|
? "before"
|
|
@@ -1061,7 +1082,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1061
1082
|
// Main cognitive loop
|
|
1062
1083
|
for (let step = 1; step <= maxSteps; step++) {
|
|
1063
1084
|
state.stepCount = step;
|
|
1064
|
-
state.timeElapsed = (Date.now() - startTime) /
|
|
1085
|
+
state.timeElapsed = (Date.now() - startTime) / param("journey.msPerSecond");
|
|
1065
1086
|
// Check time limit
|
|
1066
1087
|
if (state.timeElapsed > thresholds.timeLimit) {
|
|
1067
1088
|
abandonmentReason = "timeout";
|
|
@@ -1095,7 +1116,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1095
1116
|
try {
|
|
1096
1117
|
await Promise.race([
|
|
1097
1118
|
page.evaluate("1+1"),
|
|
1098
|
-
new Promise((_, rej) => setTimeout(() => rej(new Error("renderer health check timed out")),
|
|
1119
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("renderer health check timed out")), param("journey.healthCheckMs"))),
|
|
1099
1120
|
]);
|
|
1100
1121
|
}
|
|
1101
1122
|
catch (healthErr) {
|
|
@@ -1116,7 +1137,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1116
1137
|
const screenshotKey = currentUrl.split('?')[0]; // strip query for stable keying
|
|
1117
1138
|
const lastCapturedScrollY = lastScreenshotScroll.get(screenshotKey);
|
|
1118
1139
|
const isInitialLoadForUrl = lastCapturedScrollY === undefined;
|
|
1119
|
-
const scrolledEnough = lastCapturedScrollY !== undefined && Math.abs(viewportCtx.scroll.y - lastCapturedScrollY) >
|
|
1140
|
+
const scrolledEnough = lastCapturedScrollY !== undefined && Math.abs(viewportCtx.scroll.y - lastCapturedScrollY) > param("journey.screenshot.scrollPx");
|
|
1120
1141
|
const shouldCapture = isInitialLoadForUrl || scrolledEnough;
|
|
1121
1142
|
let screenshotPath;
|
|
1122
1143
|
let attentionHeatmapBase64;
|
|
@@ -1143,8 +1164,9 @@ Right now it's ${temporalState.description}.`;
|
|
|
1143
1164
|
// step 2 onward (step 1 has no prior to compare against).
|
|
1144
1165
|
if (lastActionOutcome) {
|
|
1145
1166
|
const currentTexts = new Set(availableElements.map(e => e.text));
|
|
1146
|
-
|
|
1147
|
-
const
|
|
1167
|
+
// Uncapped: the prompt names the first few (journey.prompt.changedElementsShown).
|
|
1168
|
+
const newClickables = [...currentTexts].filter(t => t && !prevClickableTexts.has(t));
|
|
1169
|
+
const removedClickables = [...prevClickableTexts].filter(t => t && !currentTexts.has(t));
|
|
1148
1170
|
lastActionOutcome.newClickables = newClickables;
|
|
1149
1171
|
lastActionOutcome.removedClickables = removedClickables;
|
|
1150
1172
|
// After a navigation the scroll position belongs to the new page, so the
|
|
@@ -1162,9 +1184,20 @@ Right now it's ${temporalState.description}.`;
|
|
|
1162
1184
|
// omitted the actual prices, and a long article omitted its body. The new
|
|
1163
1185
|
// version walks the visible viewport and captures every meaningful text
|
|
1164
1186
|
// node above-the-fold + a band just below, ordered top-to-bottom.
|
|
1165
|
-
|
|
1187
|
+
// The extractor runs in the page, which cannot call param(); its limits
|
|
1188
|
+
// are read here and passed in.
|
|
1189
|
+
const contentLimits = {
|
|
1190
|
+
lookaheadShare: param("journey.content.lookaheadShare"),
|
|
1191
|
+
aboveViewportPx: param("journey.content.aboveViewportPx"),
|
|
1192
|
+
minWidthPx: param("journey.content.minWidthPx"),
|
|
1193
|
+
minHeightPx: param("journey.content.minHeightPx"),
|
|
1194
|
+
minChars: param("journey.content.minChars"),
|
|
1195
|
+
maxChars: param("journey.content.maxChars"),
|
|
1196
|
+
maxLines: param("journey.content.maxLines"),
|
|
1197
|
+
};
|
|
1198
|
+
const pageContent = await page.evaluate((L) => {
|
|
1166
1199
|
const vh = window.innerHeight;
|
|
1167
|
-
const lookahead = vh *
|
|
1200
|
+
const lookahead = vh * L.lookaheadShare; // include a strip just below the fold so AI can plan ahead
|
|
1168
1201
|
const out = [];
|
|
1169
1202
|
const skipTags = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'svg', 'IFRAME']);
|
|
1170
1203
|
const nodes = document.querySelectorAll('h1, h2, h3, h4, p, li, td, th, [role="heading"], [data-testid]');
|
|
@@ -1172,20 +1205,20 @@ Right now it's ${temporalState.description}.`;
|
|
|
1172
1205
|
if (skipTags.has(el.tagName))
|
|
1173
1206
|
return;
|
|
1174
1207
|
const r = el.getBoundingClientRect();
|
|
1175
|
-
if (r.bottom < -
|
|
1208
|
+
if (r.bottom < -L.aboveViewportPx)
|
|
1176
1209
|
return; // above viewport
|
|
1177
1210
|
if (r.top > vh + lookahead)
|
|
1178
1211
|
return; // far below
|
|
1179
|
-
if (r.width <
|
|
1212
|
+
if (r.width < L.minWidthPx || r.height < L.minHeightPx)
|
|
1180
1213
|
return;
|
|
1181
1214
|
const style = window.getComputedStyle(el);
|
|
1182
1215
|
if (style.display === 'none' || style.visibility === 'hidden')
|
|
1183
1216
|
return;
|
|
1184
1217
|
const text = el.innerText?.trim();
|
|
1185
|
-
if (!text || text.length <
|
|
1218
|
+
if (!text || text.length < L.minChars)
|
|
1186
1219
|
return;
|
|
1187
1220
|
// Trim long paragraphs but keep prices/key info
|
|
1188
|
-
const trimmed = text.length >
|
|
1221
|
+
const trimmed = text.length > L.maxChars ? text.substring(0, L.maxChars) + '…' : text;
|
|
1189
1222
|
const tag = el.tagName.toLowerCase();
|
|
1190
1223
|
const tagLabel = ['h1', 'h2', 'h3', 'h4'].includes(tag) ? tag.toUpperCase() : tag;
|
|
1191
1224
|
const visMark = r.top < vh && r.bottom > 0 ? '👁' : '↓';
|
|
@@ -1201,8 +1234,8 @@ Right now it's ${temporalState.description}.`;
|
|
|
1201
1234
|
prev = o.line;
|
|
1202
1235
|
}
|
|
1203
1236
|
}
|
|
1204
|
-
return dedup.slice(0,
|
|
1205
|
-
}).catch(() => '');
|
|
1237
|
+
return dedup.slice(0, L.maxLines).join('\n');
|
|
1238
|
+
}, contentLimits).catch(() => '');
|
|
1206
1239
|
// Habituation/Banner Blindness tracking (v10.1.0)
|
|
1207
1240
|
// Detect UI patterns in available elements and update habituation state
|
|
1208
1241
|
if (state.habituation) {
|
|
@@ -1304,9 +1337,10 @@ Right now it's ${temporalState.description}.`;
|
|
|
1304
1337
|
const actionSuccess = parsed.actionSuccess !== false;
|
|
1305
1338
|
const claudeConfusion = parsed.newConfusion ?? state.emotionalState.confusion;
|
|
1306
1339
|
const claudeFrustration = parsed.newFrustration ?? state.emotionalState.frustration;
|
|
1307
|
-
const
|
|
1308
|
-
const
|
|
1309
|
-
const
|
|
1340
|
+
const signalThreshold = param("journey.signal.changeThreshold");
|
|
1341
|
+
const confusionIncreased = claudeConfusion > state.emotionalState.confusion + signalThreshold;
|
|
1342
|
+
const confusionDecreased = claudeConfusion < state.emotionalState.confusion - signalThreshold;
|
|
1343
|
+
const frustrationIncreased = claudeFrustration > state.emotionalState.frustration + signalThreshold;
|
|
1310
1344
|
// Severity scales with the magnitude of the LLM-reported delta — Claude
|
|
1311
1345
|
// proposes a value, we use the *delta* (clamped to [0, 1]) as the
|
|
1312
1346
|
// trigger's intensity, instead of trusting Claude's absolute number.
|
|
@@ -1316,7 +1350,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1316
1350
|
// matched signals can fire sequentially below if both are strong.
|
|
1317
1351
|
const triggersToFire = [];
|
|
1318
1352
|
if (!actionSuccess && parsed.errorMessage) {
|
|
1319
|
-
triggersToFire.push({ trigger: "error", severity:
|
|
1353
|
+
triggersToFire.push({ trigger: "error", severity: param("journey.severity.error"), description: parsed.errorMessage });
|
|
1320
1354
|
}
|
|
1321
1355
|
else if (!actionSuccess) {
|
|
1322
1356
|
triggersToFire.push({ trigger: "failure", severity: 1.0, description: "Action did not succeed" });
|
|
@@ -1325,7 +1359,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1325
1359
|
if (confusionIncreased) {
|
|
1326
1360
|
triggersToFire.push({
|
|
1327
1361
|
trigger: "confusion_onset",
|
|
1328
|
-
severity: Math.max(
|
|
1362
|
+
severity: Math.max(param("journey.severity.signalFloor"), confusionDelta * param("journey.severity.signalGain")),
|
|
1329
1363
|
description: parsed.frictionDescription || "UI became confusing",
|
|
1330
1364
|
});
|
|
1331
1365
|
}
|
|
@@ -1335,7 +1369,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1335
1369
|
// via decay.
|
|
1336
1370
|
triggersToFire.push({
|
|
1337
1371
|
trigger: "clarity",
|
|
1338
|
-
severity:
|
|
1372
|
+
severity: param("journey.severity.clarity"),
|
|
1339
1373
|
description: "UI became clear",
|
|
1340
1374
|
});
|
|
1341
1375
|
}
|
|
@@ -1343,24 +1377,24 @@ Right now it's ${temporalState.description}.`;
|
|
|
1343
1377
|
// Friction without outright failure
|
|
1344
1378
|
triggersToFire.push({
|
|
1345
1379
|
trigger: "setback",
|
|
1346
|
-
severity: Math.max(
|
|
1380
|
+
severity: Math.max(param("journey.severity.signalFloor"), frustrationDelta * param("journey.severity.signalGain")),
|
|
1347
1381
|
description: "Encountered friction",
|
|
1348
1382
|
});
|
|
1349
1383
|
}
|
|
1350
1384
|
// Time pressure fires when patience is already derived-low (recompute
|
|
1351
1385
|
// briefly here using current state for the gating check).
|
|
1352
1386
|
const patiencePreview = deriveSimplifiedState(state.emotionalState, traits).patienceRemaining;
|
|
1353
|
-
if (patiencePreview <
|
|
1387
|
+
if (patiencePreview < param("journey.timePressure.patience") && !triggersToFire.length) {
|
|
1354
1388
|
triggersToFire.push({
|
|
1355
1389
|
trigger: "time_pressure",
|
|
1356
|
-
severity:
|
|
1390
|
+
severity: param("journey.severity.timePressure"),
|
|
1357
1391
|
description: "Running out of patience",
|
|
1358
1392
|
});
|
|
1359
1393
|
}
|
|
1360
1394
|
if (actionSuccess && !triggersToFire.length) {
|
|
1361
1395
|
triggersToFire.push({
|
|
1362
1396
|
trigger: "success",
|
|
1363
|
-
severity:
|
|
1397
|
+
severity: param("journey.severity.success"),
|
|
1364
1398
|
description: parsed.action || "Action completed",
|
|
1365
1399
|
});
|
|
1366
1400
|
}
|
|
@@ -1401,7 +1435,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1401
1435
|
if (wasInteractive && actionSucceeded) {
|
|
1402
1436
|
// Allow up to the LLM's proposed delta; URL-change reconciliation later
|
|
1403
1437
|
// expands the cap to full LLM proposal if URL changed.
|
|
1404
|
-
progressDeltaAllowed = Math.min(progressDeltaProposed,
|
|
1438
|
+
progressDeltaAllowed = Math.min(progressDeltaProposed, param("journey.progress.inPageCap"));
|
|
1405
1439
|
}
|
|
1406
1440
|
// No evidence at all → no progress (Claude was hallucinating advancement)
|
|
1407
1441
|
}
|
|
@@ -1466,10 +1500,11 @@ Right now it's ${temporalState.description}.`;
|
|
|
1466
1500
|
const cognitiveLoad = (state.confusionLevel + state.frustrationLevel) / 2;
|
|
1467
1501
|
const prevWidth = state.scanPattern.widthMultiplier;
|
|
1468
1502
|
state.scanPattern.widthMultiplier = calculateScanWidthMultiplier(cognitiveLoad);
|
|
1469
|
-
state.scanPattern.effectiveWidth = state.scanPattern.widthMultiplier *
|
|
1503
|
+
state.scanPattern.effectiveWidth = state.scanPattern.widthMultiplier * param("journey.percentScale");
|
|
1470
1504
|
// Log if pattern narrowed significantly
|
|
1471
1505
|
if (options.verbose && prevWidth !== state.scanPattern.widthMultiplier) {
|
|
1472
|
-
|
|
1506
|
+
// The scan width takes only its registered values; the two narrowest are tunnel vision.
|
|
1507
|
+
if (state.scanPattern.widthMultiplier <= param("journey.scan.highWidth")) {
|
|
1473
1508
|
console.log(`👁️ Tunnel vision: scanning only left ${state.scanPattern.effectiveWidth.toFixed(0)}% of viewport`);
|
|
1474
1509
|
}
|
|
1475
1510
|
else if (prevWidth < state.scanPattern.widthMultiplier) {
|
|
@@ -1482,8 +1517,9 @@ Right now it's ${temporalState.description}.`;
|
|
|
1482
1517
|
const prevArousal = state.peripheralVision.arousalLevel;
|
|
1483
1518
|
state.peripheralVision = calculatePeripheralVision(state.frustrationLevel, state.confusionLevel);
|
|
1484
1519
|
// Log significant arousal changes
|
|
1485
|
-
if (options.verbose && Math.abs(state.peripheralVision.arousalLevel - prevArousal) >
|
|
1486
|
-
|
|
1520
|
+
if (options.verbose && Math.abs(state.peripheralVision.arousalLevel - prevArousal) > param("journey.log.arousalChange")) {
|
|
1521
|
+
// The field takes only its registered factors; the severe one is reported.
|
|
1522
|
+
if (state.peripheralVision.widthFactor <= param("journey.peripheral.severeWidth")) {
|
|
1487
1523
|
console.log(`😰 Stress tunnel vision: peripheral field reduced to ${(state.peripheralVision.widthFactor * 100).toFixed(0)}%`);
|
|
1488
1524
|
}
|
|
1489
1525
|
}
|
|
@@ -1516,13 +1552,14 @@ Right now it's ${temporalState.description}.`;
|
|
|
1516
1552
|
viewport: { width: viewportCtx.viewport.width, height: viewportCtx.viewport.height },
|
|
1517
1553
|
});
|
|
1518
1554
|
// Record friction point if confusion/frustration spiked
|
|
1519
|
-
|
|
1555
|
+
const frictionLevel = param("journey.friction.level");
|
|
1556
|
+
if (parsed.frictionDescription && (state.confusionLevel > frictionLevel || state.frustrationLevel > frictionLevel)) {
|
|
1520
1557
|
frictionPoints.push({
|
|
1521
1558
|
step,
|
|
1522
1559
|
url: currentUrl,
|
|
1523
1560
|
element: parsed.frictionElement,
|
|
1524
1561
|
type: "confusing_ui",
|
|
1525
|
-
frustrationIncrease: parsed.newFrustration ? parsed.newFrustration - state.frustrationLevel :
|
|
1562
|
+
frustrationIncrease: parsed.newFrustration ? parsed.newFrustration - state.frustrationLevel : param("journey.friction.defaultIncrease"),
|
|
1526
1563
|
monologue: parsed.monologue || "",
|
|
1527
1564
|
});
|
|
1528
1565
|
}
|
|
@@ -1546,13 +1583,9 @@ Right now it's ${temporalState.description}.`;
|
|
|
1546
1583
|
url: currentUrl,
|
|
1547
1584
|
});
|
|
1548
1585
|
}
|
|
1549
|
-
//
|
|
1550
|
-
//
|
|
1551
|
-
// heatmap
|
|
1552
|
-
// sluggish. Skipped on non-capture steps so the journey paces normally.
|
|
1553
|
-
if (options.enableMouseOverlay && attentionHeatmapBase64) {
|
|
1554
|
-
await new Promise((r) => setTimeout(r, 1200));
|
|
1555
|
-
}
|
|
1586
|
+
// (A 1.2 s live-viewer dwell after a step with a fresh heatmap stood
|
|
1587
|
+
// here until 21.0. Nothing assigns attentionHeatmapBase64 since the
|
|
1588
|
+
// heatmap moved into the page, so the pause could never run.)
|
|
1556
1589
|
// Verbose output
|
|
1557
1590
|
if (options.verbose) {
|
|
1558
1591
|
console.log(`\n━━━ Step ${step} ━━━`);
|
|
@@ -1570,11 +1603,12 @@ Right now it's ${temporalState.description}.`;
|
|
|
1570
1603
|
// v18.29.0: Validate goal evidence against actual page content
|
|
1571
1604
|
if (goalEvidence) {
|
|
1572
1605
|
try {
|
|
1573
|
-
const pageText = await page.evaluate(() => document.body.innerText?.substring(0,
|
|
1574
|
-
const
|
|
1606
|
+
const pageText = await page.evaluate((n) => document.body.innerText?.substring(0, n) || '', param("journey.evidence.pageChars"));
|
|
1607
|
+
const minWordChars = param("journey.evidence.minWordChars");
|
|
1608
|
+
const evidenceWords = goalEvidence.toLowerCase().split(/\s+/).filter((w) => w.length > minWordChars);
|
|
1575
1609
|
const matchCount = evidenceWords.filter((w) => pageText.toLowerCase().includes(w)).length;
|
|
1576
1610
|
const matchRatio = evidenceWords.length > 0 ? matchCount / evidenceWords.length : 0;
|
|
1577
|
-
if (matchRatio <
|
|
1611
|
+
if (matchRatio < param("journey.evidence.verifiedShare")) {
|
|
1578
1612
|
// Evidence doesn't match page content — flag as unverified
|
|
1579
1613
|
goalEvidence = `[UNVERIFIED - ${Math.round(matchRatio * 100)}% match] ${goalEvidence}`;
|
|
1580
1614
|
}
|
|
@@ -1594,8 +1628,8 @@ Right now it's ${temporalState.description}.`;
|
|
|
1594
1628
|
fullMonologue.push(abandonment.message);
|
|
1595
1629
|
// v18.29.0: Capture failure context
|
|
1596
1630
|
try {
|
|
1597
|
-
const pageText = await page.evaluate(() => document.body.innerText?.substring(0,
|
|
1598
|
-
failureReason = parsed.failureReason || `Abandoned on "${pageTitle}" (${currentUrl}). Last visible: ${pageText
|
|
1631
|
+
const pageText = await page.evaluate((n) => document.body.innerText?.substring(0, n) || '', param("journey.quoteChars"));
|
|
1632
|
+
failureReason = parsed.failureReason || `Abandoned on "${pageTitle}" (${currentUrl}). Last visible: ${pageText}...`;
|
|
1599
1633
|
}
|
|
1600
1634
|
catch {
|
|
1601
1635
|
failureReason = parsed.failureReason || `Abandoned on "${pageTitle}" (${currentUrl})`;
|
|
@@ -1617,7 +1651,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1617
1651
|
// @ts-expect-error window helper installed by overlay script
|
|
1618
1652
|
if (window.__cbShowAction)
|
|
1619
1653
|
window.__cbShowAction(t);
|
|
1620
|
-
}, label.slice(0,
|
|
1654
|
+
}, label.slice(0, param("journey.live.bannerChars")));
|
|
1621
1655
|
}
|
|
1622
1656
|
catch { /* page navigated mid-call, helper will reinstall */ }
|
|
1623
1657
|
}
|
|
@@ -1629,41 +1663,8 @@ Right now it's ${temporalState.description}.`;
|
|
|
1629
1663
|
target = availableElements[parseInt(indexMatch[1], 10)];
|
|
1630
1664
|
}
|
|
1631
1665
|
else if (textMatch && !textMatch[1].startsWith("#")) {
|
|
1632
|
-
// Find best element match by text
|
|
1633
|
-
|
|
1634
|
-
// closest to the current viewport center.
|
|
1635
|
-
const wanted = textMatch[1].trim().toLowerCase();
|
|
1636
|
-
const vc = viewportCtx.viewport.height / 2;
|
|
1637
|
-
const candidates = availableElements.filter(e => e.text);
|
|
1638
|
-
const score = (e) => {
|
|
1639
|
-
const t = (e.text || "").toLowerCase().trim();
|
|
1640
|
-
if (!t)
|
|
1641
|
-
return -Infinity;
|
|
1642
|
-
let base = 0;
|
|
1643
|
-
if (t === wanted)
|
|
1644
|
-
base = 1000;
|
|
1645
|
-
else if (t.startsWith(wanted) || wanted.startsWith(t))
|
|
1646
|
-
base = 500;
|
|
1647
|
-
else if (t.includes(wanted) || wanted.includes(t))
|
|
1648
|
-
base = 100;
|
|
1649
|
-
else
|
|
1650
|
-
return -Infinity;
|
|
1651
|
-
// Penalize distance from viewport center (closer = preferred)
|
|
1652
|
-
const ey = (e.y ?? 0) + (e.height ?? 0) / 2;
|
|
1653
|
-
const dist = Math.abs(ey - vc);
|
|
1654
|
-
return base - Math.min(dist, 5000);
|
|
1655
|
-
};
|
|
1656
|
-
let best;
|
|
1657
|
-
let bestScore = -Infinity;
|
|
1658
|
-
for (const e of candidates) {
|
|
1659
|
-
const s = score(e);
|
|
1660
|
-
if (s > bestScore) {
|
|
1661
|
-
bestScore = s;
|
|
1662
|
-
best = e;
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
if (best && bestScore > 0)
|
|
1666
|
-
target = best;
|
|
1666
|
+
// Find best element match by text (matchClickable).
|
|
1667
|
+
target = matchClickable(availableElements, textMatch[1], viewportCtx.viewport.height);
|
|
1667
1668
|
}
|
|
1668
1669
|
let result;
|
|
1669
1670
|
const hasBbox = target && typeof target.x === "number" && typeof target.y === "number"
|
|
@@ -1673,6 +1674,9 @@ Right now it's ${temporalState.description}.`;
|
|
|
1673
1674
|
const urlBefore = page.url();
|
|
1674
1675
|
const cx = target.x + target.width / 2;
|
|
1675
1676
|
const cy = target.y + target.height / 2;
|
|
1677
|
+
// Set once the pointing wait below is paid, so the retry by name
|
|
1678
|
+
// after a throw does not pay it again.
|
|
1679
|
+
let pointingPaid = false;
|
|
1676
1680
|
try {
|
|
1677
1681
|
// Only scroll if the element is OFF-screen. Scrolling an already-
|
|
1678
1682
|
// visible element causes pointless jitter that stacks with any
|
|
@@ -1685,7 +1689,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1685
1689
|
// "instant" behavior — no animation, no stacking with page handlers
|
|
1686
1690
|
window.scrollTo({ top: t, behavior: "instant" });
|
|
1687
1691
|
}, { y: cy });
|
|
1688
|
-
await page.waitForTimeout(
|
|
1692
|
+
await page.waitForTimeout(param("journey.click.scrollSettleMs"));
|
|
1689
1693
|
}
|
|
1690
1694
|
// Re-read post-scroll coords; click at center via raw mouse so
|
|
1691
1695
|
// Playwright's locator auto-scroll-into-view never engages.
|
|
@@ -1694,11 +1698,18 @@ Right now it's ${temporalState.description}.`;
|
|
|
1694
1698
|
if (!el)
|
|
1695
1699
|
return null;
|
|
1696
1700
|
const r = el.getBoundingClientRect();
|
|
1697
|
-
return { x: r.left + r.width / 2, y: r.top + r.height / 2, w: r.width, h: r.height };
|
|
1701
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2, w: r.width, h: r.height, left: r.left, top: r.top, scrollY: window.scrollY };
|
|
1698
1702
|
}, { sel: target.selector });
|
|
1699
1703
|
const ctr = (fresh && fresh.x > 0 && fresh.y > 0)
|
|
1700
1704
|
? { x: fresh.x, y: fresh.y, w: fresh.w, h: fresh.h }
|
|
1701
1705
|
: { x: cx, y: cy, w: target.width, h: target.height };
|
|
1706
|
+
// The pointer's move to the box it is sent to, as a hover's: Stage
|
|
1707
|
+
// B's law from the centre of the screen, plus the gaze lag.
|
|
1708
|
+
const sentTo = (fresh && fresh.x > 0 && fresh.y > 0)
|
|
1709
|
+
? boxGeometry({ ...target, x: fresh.left, y: fresh.top, width: fresh.w, height: fresh.h }, viewportCtx.viewport, fresh.scrollY)
|
|
1710
|
+
: boxGeometry(target, viewportCtx.viewport, viewportCtx.scroll.y);
|
|
1711
|
+
await pointingWait(fittsParams, state.gazeMouseLag, options.humanInput, sentTo);
|
|
1712
|
+
pointingPaid = true;
|
|
1702
1713
|
if (options.humanInput) {
|
|
1703
1714
|
const { humanClick } = await import("../visual/human-input.js");
|
|
1704
1715
|
await humanClick(page, ctr.x, ctr.y, inputTraits, { width: ctr.w, height: ctr.h });
|
|
@@ -1706,20 +1717,21 @@ Right now it's ${temporalState.description}.`;
|
|
|
1706
1717
|
else {
|
|
1707
1718
|
await page.mouse.click(ctr.x, ctr.y);
|
|
1708
1719
|
}
|
|
1709
|
-
await page.waitForTimeout(
|
|
1720
|
+
await page.waitForTimeout(param("journey.action.navigationWaitMs"));
|
|
1710
1721
|
const urlAfter = page.url();
|
|
1711
1722
|
result = { success: true, ...(urlAfter !== urlBefore ? { newUrl: urlAfter } : {}) };
|
|
1712
1723
|
}
|
|
1713
1724
|
catch (clickErr) {
|
|
1714
1725
|
console.warn(`[cognitive] coordinate click failed: ${clickErr.message}`);
|
|
1715
1726
|
const fallback = target.text ? `click:${target.text}` : parsed.action;
|
|
1716
|
-
result = await executeAction(browser, fallback, fittsParams, state.gazeMouseLag, options.humanInput, inputTraits);
|
|
1727
|
+
result = await executeAction(browser, fallback, fittsParams, state.gazeMouseLag, options.humanInput, inputTraits, boxGeometry(target, viewportCtx.viewport, viewportCtx.scroll.y), pointingPaid);
|
|
1717
1728
|
}
|
|
1718
1729
|
}
|
|
1719
1730
|
else {
|
|
1720
1731
|
// No bbox — let executeAction handle (non-click actions, or click
|
|
1721
|
-
// text not found in availableElements).
|
|
1722
|
-
|
|
1732
|
+
// text not found in availableElements). A hover names its element
|
|
1733
|
+
// the same way; its listed geometry, if any, times the pointing.
|
|
1734
|
+
result = await executeAction(browser, parsed.action, fittsParams, state.gazeMouseLag, options.humanInput, inputTraits, pointingGeometry(parsed.action, availableElements, viewportCtx.viewport, viewportCtx.scroll.y));
|
|
1723
1735
|
}
|
|
1724
1736
|
// Inter-action settle pause — humans don't fire actions back-to-back
|
|
1725
1737
|
// at machine speed. Modulated by patience + readingTendency so an
|
|
@@ -1759,8 +1771,9 @@ Right now it's ${temporalState.description}.`;
|
|
|
1759
1771
|
// Goal progress cap was previously +0.05 max (no-evidence fallback).
|
|
1760
1772
|
// If the URL actually changed, expand the cap to the LLM's full
|
|
1761
1773
|
// proposed delta — that's strong navigation evidence.
|
|
1762
|
-
|
|
1763
|
-
|
|
1774
|
+
const inPageCap = param("journey.progress.inPageCap");
|
|
1775
|
+
if (urlChanged && _llmProposedProgressDelta > inPageCap) {
|
|
1776
|
+
const additionalAllowed = _llmProposedProgressDelta - inPageCap;
|
|
1764
1777
|
state.goalProgress = Math.min(1, state.goalProgress + additionalAllowed);
|
|
1765
1778
|
}
|
|
1766
1779
|
// Action result feeds back into the canonical EmotionalState via triggers.
|
|
@@ -1768,10 +1781,10 @@ Right now it's ${temporalState.description}.`;
|
|
|
1768
1781
|
// which were overwritten next step by deriveSimplifiedState anyway.
|
|
1769
1782
|
if (state.emotionalState) {
|
|
1770
1783
|
if (result.success) {
|
|
1771
|
-
const progressMade = urlChanged || state.goalProgress >
|
|
1784
|
+
const progressMade = urlChanged || state.goalProgress > param("journey.progress.madeLevel");
|
|
1772
1785
|
// Progress = stronger positive trigger than plain success
|
|
1773
1786
|
const { state: newEmotional, event } = applyEmotionalTrigger(state.emotionalState, progressMade ? "progress" : "success", emotionalConfig, state.stepCount, {
|
|
1774
|
-
severity: progressMade ?
|
|
1787
|
+
severity: progressMade ? param("journey.severity.progress") : param("journey.severity.success"),
|
|
1775
1788
|
description: progressMade
|
|
1776
1789
|
? `Action advanced goal (${urlChanged ? "URL change" : "in-page progress"})`
|
|
1777
1790
|
: `Action completed: ${parsed.action}`,
|
|
@@ -1798,7 +1811,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1798
1811
|
// Action threw — fire an error trigger on canonical state.
|
|
1799
1812
|
if (state.emotionalState) {
|
|
1800
1813
|
const { state: newEmotional, event } = applyEmotionalTrigger(state.emotionalState, "error", emotionalConfig, state.stepCount, {
|
|
1801
|
-
severity:
|
|
1814
|
+
severity: param("journey.severity.actionThrew"),
|
|
1802
1815
|
description: error instanceof Error ? error.message : String(error),
|
|
1803
1816
|
});
|
|
1804
1817
|
state.emotionalState = newEmotional;
|
|
@@ -1822,15 +1835,15 @@ Right now it's ${temporalState.description}.`;
|
|
|
1822
1835
|
// Estimate decision complexity: clicks on links = easy (2 options),
|
|
1823
1836
|
// form fills = medium (5), navigation choices = hard (8+)
|
|
1824
1837
|
const estimatedOptions = parsed.action.toLowerCase().includes("fill")
|
|
1825
|
-
?
|
|
1838
|
+
? param("journey.fatigue.options.fill")
|
|
1826
1839
|
: parsed.action.toLowerCase().includes("navigate") || parsed.action.toLowerCase().includes("search")
|
|
1827
|
-
?
|
|
1828
|
-
:
|
|
1840
|
+
? param("journey.fatigue.options.navigate")
|
|
1841
|
+
: param("journey.fatigue.options.other");
|
|
1829
1842
|
const fatigueIncrement = calculateFatigueIncrement(estimatedOptions);
|
|
1830
1843
|
state.decisionFatigue.fatigueLevel = Math.min(1, state.decisionFatigue.fatigueLevel + fatigueIncrement);
|
|
1831
1844
|
state.decisionFatigue.lastDecisionComplexity = estimatedOptions;
|
|
1832
1845
|
// High fatigue triggers default-seeking behavior
|
|
1833
|
-
if (state.decisionFatigue.fatigueLevel >
|
|
1846
|
+
if (state.decisionFatigue.fatigueLevel > param("journey.fatigue.defaultsLevel")) {
|
|
1834
1847
|
state.decisionFatigue.choosingDefaults = true;
|
|
1835
1848
|
}
|
|
1836
1849
|
if (options.verbose) {
|
|
@@ -1839,13 +1852,13 @@ Right now it's ${temporalState.description}.`;
|
|
|
1839
1852
|
}
|
|
1840
1853
|
}
|
|
1841
1854
|
// Small delay between steps
|
|
1842
|
-
await sleep(
|
|
1855
|
+
await sleep(param("journey.stepPauseMs"));
|
|
1843
1856
|
}
|
|
1844
1857
|
// Post-journey goal validation — final evaluation of the current page
|
|
1845
1858
|
if (!goalAchieved) {
|
|
1846
1859
|
try {
|
|
1847
1860
|
const page = await browser.getPage();
|
|
1848
|
-
const finalPageText = await page.evaluate(() => document.body.innerText?.substring(0,
|
|
1861
|
+
const finalPageText = await page.evaluate((n) => document.body.innerText?.substring(0, n) || '', param("journey.finalEval.pageChars"));
|
|
1849
1862
|
const finalUrl = page.url();
|
|
1850
1863
|
// Ask Claude: did we actually achieve the goal? Through the decision
|
|
1851
1864
|
// provider, recorded as the trace's final step, so a replay answers
|
|
@@ -1888,7 +1901,7 @@ Answer with JSON:
|
|
|
1888
1901
|
const evalJson = (readReplyJson(evalText).value ?? {});
|
|
1889
1902
|
if (evalJson.achieved) {
|
|
1890
1903
|
goalAchieved = true;
|
|
1891
|
-
goalEvidence = `[FINAL EVAL] ${evalJson.evidence || finalPageText.substring(0,
|
|
1904
|
+
goalEvidence = `[FINAL EVAL] ${evalJson.evidence || finalPageText.substring(0, param("journey.quoteChars"))}`;
|
|
1892
1905
|
abandonmentReason = undefined;
|
|
1893
1906
|
abandonmentMessage = undefined;
|
|
1894
1907
|
}
|
|
@@ -1898,13 +1911,13 @@ Answer with JSON:
|
|
|
1898
1911
|
catch { }
|
|
1899
1912
|
}
|
|
1900
1913
|
// Fallback: only if goalProgress is very high (90%+) AND final monologue explicitly says "found"
|
|
1901
|
-
if (!goalAchieved && state.goalProgress >=
|
|
1914
|
+
if (!goalAchieved && state.goalProgress >= param("journey.fallback.progress")) {
|
|
1902
1915
|
const lastMonologue = (fullMonologue[fullMonologue.length - 1] || '').toLowerCase();
|
|
1903
1916
|
const strongSignals = ['found it', 'found exactly', 'this is exactly', 'goal achieved', 'here it is', 'perfect, this is'];
|
|
1904
1917
|
const hasStrongSignal = strongSignals.some(s => lastMonologue.includes(s));
|
|
1905
1918
|
if (hasStrongSignal) {
|
|
1906
1919
|
goalAchieved = true;
|
|
1907
|
-
goalEvidence = `[AUTO-DETECTED: goalProgress=${Math.round(state.goalProgress * 100)}%] ${fullMonologue[fullMonologue.length - 1]?.substring(0,
|
|
1920
|
+
goalEvidence = `[AUTO-DETECTED: goalProgress=${Math.round(state.goalProgress * 100)}%] ${fullMonologue[fullMonologue.length - 1]?.substring(0, param("journey.quoteChars")) || ''}`;
|
|
1908
1921
|
abandonmentReason = undefined;
|
|
1909
1922
|
abandonmentMessage = undefined;
|
|
1910
1923
|
}
|
|
@@ -1927,7 +1940,7 @@ Answer with JSON:
|
|
|
1927
1940
|
...(traceWriter ? { tracePath: traceWriter.filePath } : {}),
|
|
1928
1941
|
abandonmentReason,
|
|
1929
1942
|
abandonmentMessage,
|
|
1930
|
-
totalTime: (Date.now() - startTime) /
|
|
1943
|
+
totalTime: (Date.now() - startTime) / param("journey.msPerSecond"),
|
|
1931
1944
|
stepCount: state.stepCount,
|
|
1932
1945
|
frictionPoints,
|
|
1933
1946
|
fullMonologue,
|
|
@@ -1936,7 +1949,7 @@ Answer with JSON:
|
|
|
1936
1949
|
avgConfusionLevel: emotion.avgConfusionLevel,
|
|
1937
1950
|
maxFrustrationLevel: emotion.maxFrustrationLevel,
|
|
1938
1951
|
backtrackCount: state.memory.backtrackCount,
|
|
1939
|
-
timeInConfusion: frictionPoints.length *
|
|
1952
|
+
timeInConfusion: frictionPoints.length * param("journey.summary.secondsPerFriction"), // Estimate
|
|
1940
1953
|
// Decision fatigue metrics (v9.9.0)
|
|
1941
1954
|
decisionsMade: state.decisionFatigue?.decisionsMade ?? 0,
|
|
1942
1955
|
finalDecisionFatigue: state.decisionFatigue?.fatigueLevel ?? 0,
|
|
@@ -1965,17 +1978,21 @@ Answer with JSON:
|
|
|
1965
1978
|
// ============================================================================
|
|
1966
1979
|
function buildCognitiveSystemPrompt(persona, profile, goal, thresholds) {
|
|
1967
1980
|
const traits = profile.traits;
|
|
1981
|
+
const lowP = param("journey.band.low.patience"), highP = param("journey.band.high.patience");
|
|
1982
|
+
const lowR = param("journey.band.low.riskTolerance"), highR = param("journey.band.high.riskTolerance");
|
|
1983
|
+
const lowC = param("journey.band.low.comprehension"), highC = param("journey.band.high.comprehension");
|
|
1984
|
+
const lowT = param("journey.band.low.readingTendency"), highT = param("journey.band.high.readingTendency");
|
|
1968
1985
|
return `You are simulating a "${persona.name}" user navigating a website.
|
|
1969
1986
|
|
|
1970
1987
|
PERSONA DESCRIPTION: ${persona.description}
|
|
1971
1988
|
|
|
1972
1989
|
COGNITIVE TRAITS:
|
|
1973
|
-
- Patience: ${traits.patience.toFixed(2)} ${traits.patience <
|
|
1974
|
-
- Risk Tolerance: ${traits.riskTolerance.toFixed(2)} ${traits.riskTolerance <
|
|
1975
|
-
- Comprehension: ${traits.comprehension.toFixed(2)} ${traits.comprehension <
|
|
1990
|
+
- Patience: ${traits.patience.toFixed(2)} ${traits.patience < lowP ? "(impatient)" : traits.patience > highP ? "(patient)" : "(moderate)"}
|
|
1991
|
+
- Risk Tolerance: ${traits.riskTolerance.toFixed(2)} ${traits.riskTolerance < lowR ? "(cautious)" : traits.riskTolerance > highR ? "(bold)" : "(moderate)"}
|
|
1992
|
+
- Comprehension: ${traits.comprehension.toFixed(2)} ${traits.comprehension < lowC ? "(struggles with UI)" : traits.comprehension > highC ? "(expert)" : "(moderate)"}
|
|
1976
1993
|
- Persistence: ${traits.persistence.toFixed(2)}
|
|
1977
1994
|
- Curiosity: ${traits.curiosity.toFixed(2)}
|
|
1978
|
-
- Reading Tendency: ${traits.readingTendency.toFixed(2)} ${traits.readingTendency <
|
|
1995
|
+
- Reading Tendency: ${traits.readingTendency.toFixed(2)} ${traits.readingTendency < lowT ? "(scans only)" : traits.readingTendency > highT ? "(reads everything)" : "(selective reader)"}
|
|
1979
1996
|
|
|
1980
1997
|
ATTENTION PATTERN: ${profile.attentionPattern}
|
|
1981
1998
|
DECISION STYLE: ${profile.decisionStyle}
|
|
@@ -2085,7 +2102,7 @@ function buildStepPrompt(state, currentUrl, pageTitle, step, availableElements =
|
|
|
2085
2102
|
let viewportStr = "";
|
|
2086
2103
|
if (viewportCtx) {
|
|
2087
2104
|
const { viewport, scroll, headings, activeModals, formErrors, bannersBlocking } = viewportCtx;
|
|
2088
|
-
const visHeadings = headings.filter(h => h.visible).slice(0,
|
|
2105
|
+
const visHeadings = headings.filter(h => h.visible).slice(0, param("journey.prompt.headings")).map(h => `H${h.level}: ${h.text}`).join(' | ');
|
|
2089
2106
|
const lines = [
|
|
2090
2107
|
`- Viewport: ${viewport.width}×${viewport.height}`,
|
|
2091
2108
|
`- Scroll: ${scroll.y}/${scroll.max}px (${Math.round(scroll.pct * 100)}%)${scroll.atTop ? ' [at top]' : ''}${scroll.atBottom ? ' [at bottom]' : ''}`,
|
|
@@ -2113,15 +2130,15 @@ function buildStepPrompt(state, currentUrl, pageTitle, step, availableElements =
|
|
|
2113
2130
|
else if (lastOutcome.action.startsWith('click')) {
|
|
2114
2131
|
lines.push(`- Page did NOT navigate (click had no nav effect — element may be JS-only or wasn't matched)`);
|
|
2115
2132
|
}
|
|
2116
|
-
if (typeof lastOutcome.scrollDelta === 'number' && Math.abs(lastOutcome.scrollDelta) >
|
|
2133
|
+
if (typeof lastOutcome.scrollDelta === 'number' && Math.abs(lastOutcome.scrollDelta) > param("journey.prompt.scrollReportPx")) {
|
|
2117
2134
|
const dir = lastOutcome.scrollDelta > 0 ? 'down' : 'up';
|
|
2118
2135
|
lines.push(`- Scrolled ${dir} ${Math.abs(lastOutcome.scrollDelta)}px`);
|
|
2119
2136
|
}
|
|
2120
2137
|
if (lastOutcome.newClickables?.length) {
|
|
2121
|
-
lines.push(`- New elements appeared: ${lastOutcome.newClickables.slice(0,
|
|
2138
|
+
lines.push(`- New elements appeared: ${lastOutcome.newClickables.slice(0, param("journey.prompt.changedElementsShown")).map(t => `"${t}"`).join(', ')}`);
|
|
2122
2139
|
}
|
|
2123
2140
|
if (lastOutcome.removedClickables?.length) {
|
|
2124
|
-
lines.push(`- Elements no longer visible: ${lastOutcome.removedClickables.slice(0,
|
|
2141
|
+
lines.push(`- Elements no longer visible: ${lastOutcome.removedClickables.slice(0, param("journey.prompt.changedElementsShown")).map(t => `"${t}"`).join(', ')}`);
|
|
2125
2142
|
}
|
|
2126
2143
|
outcomeStr = `\nWHAT HAPPENED LAST STEP:\n${lines.join('\n')}\n`;
|
|
2127
2144
|
}
|
|
@@ -2148,14 +2165,16 @@ CURRENT STATE:
|
|
|
2148
2165
|
${(() => {
|
|
2149
2166
|
// Loop detection: if the last 4 actions are alternating scrolls, surface
|
|
2150
2167
|
// it loudly so the AI breaks out instead of pumping scroll:down/up forever.
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
|
|
2168
|
+
const scrollLoop = param("journey.prompt.scrollLoop");
|
|
2169
|
+
const recent = state.memory.actionsAttempted.slice(-scrollLoop).map(a => a.action || "");
|
|
2170
|
+
if (recent.length >= scrollLoop && recent.every(a => a.startsWith("scroll:"))) {
|
|
2171
|
+
return `\n⚠️ LOOP DETECTED: your last ${scrollLoop} actions were all scrolls (${recent.join(", ")}). Pick a click/back/dismiss/find action instead — scrolling more will not help.`;
|
|
2154
2172
|
}
|
|
2155
2173
|
// Repeated failed actions
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2158
|
-
|
|
2174
|
+
const failedRun = param("journey.prompt.failedRun");
|
|
2175
|
+
const lastTwo = state.memory.actionsAttempted.slice(-failedRun);
|
|
2176
|
+
if (lastTwo.length === failedRun && lastTwo.every(a => a.success === false)) {
|
|
2177
|
+
return `\n⚠️ Last ${failedRun} actions failed (${lastTwo.map(a => a.action).join(", ")}). The element you wanted may not exist with that exact text — try a click:#N index from AVAILABLE ELEMENTS above, or a different element entirely.`;
|
|
2159
2178
|
}
|
|
2160
2179
|
return "";
|
|
2161
2180
|
})()}
|
|
@@ -2232,37 +2251,7 @@ export function resolveJourneyPersona(name, customTraits) {
|
|
|
2232
2251
|
else if (customTraits) {
|
|
2233
2252
|
// v16.7.1: full default trait set (was only 7, causing trait dropout).
|
|
2234
2253
|
// Said 25 until 2026-08-01; the model has 26.
|
|
2235
|
-
const defaultTraits =
|
|
2236
|
-
// Required traits (7)
|
|
2237
|
-
patience: 0.5,
|
|
2238
|
-
riskTolerance: 0.5,
|
|
2239
|
-
comprehension: 0.5,
|
|
2240
|
-
persistence: 0.5,
|
|
2241
|
-
curiosity: 0.5,
|
|
2242
|
-
workingMemory: 0.5,
|
|
2243
|
-
readingTendency: 0.5,
|
|
2244
|
-
// Optional traits - Tier 1: Core
|
|
2245
|
-
resilience: 0.5,
|
|
2246
|
-
selfEfficacy: 0.5,
|
|
2247
|
-
satisficing: 0.5,
|
|
2248
|
-
trustCalibration: 0.5,
|
|
2249
|
-
interruptRecovery: 0.5,
|
|
2250
|
-
// Optional traits - Tier 2+
|
|
2251
|
-
informationForaging: 0.5,
|
|
2252
|
-
changeBlindness: 0.3,
|
|
2253
|
-
anchoringBias: 0.5,
|
|
2254
|
-
timeHorizon: 0.5,
|
|
2255
|
-
attributionStyle: 0.5,
|
|
2256
|
-
metacognitivePlanning: 0.5,
|
|
2257
|
-
proceduralFluency: 0.5,
|
|
2258
|
-
transferLearning: 0.5,
|
|
2259
|
-
authoritySensitivity: 0.5,
|
|
2260
|
-
emotionalContagion: 0.5,
|
|
2261
|
-
fearOfMissingOut: 0.5,
|
|
2262
|
-
socialProofSensitivity: 0.5,
|
|
2263
|
-
mentalModelFlexibility: 0.5,
|
|
2264
|
-
siteFamiliarity: 0.5,
|
|
2265
|
-
};
|
|
2254
|
+
const defaultTraits = defaultJourneyTraits();
|
|
2266
2255
|
personaObj = {
|
|
2267
2256
|
...existingPersona,
|
|
2268
2257
|
cognitiveTraits: {
|
|
@@ -2291,7 +2280,7 @@ export function createLiveDecisionProvider(anthropic, model) {
|
|
|
2291
2280
|
decide: async (ctx) => {
|
|
2292
2281
|
if (!anthropic)
|
|
2293
2282
|
throw new Error("Anthropic API key not configured.");
|
|
2294
|
-
const ask = (messages, maxTokens =
|
|
2283
|
+
const ask = (messages, maxTokens = param("journey.llm.maxTokens")) => anthropic.messages.create({
|
|
2295
2284
|
model,
|
|
2296
2285
|
max_tokens: maxTokens,
|
|
2297
2286
|
...(ctx.systemPrompt ? { system: ctx.systemPrompt } : {}),
|
|
@@ -2315,7 +2304,7 @@ export function createLiveDecisionProvider(anthropic, model) {
|
|
|
2315
2304
|
...ctx.messages,
|
|
2316
2305
|
{ role: "assistant", content: firstText.trim() || "(no text)" },
|
|
2317
2306
|
{ role: "user", content: "Your reply could not be read as the JSON object the response format asks for. Reply again with only that JSON object, nothing before or after it." },
|
|
2318
|
-
], first.stop_reason === "max_tokens" ?
|
|
2307
|
+
], first.stop_reason === "max_tokens" ? param("journey.llm.retryMaxTokens") : param("journey.llm.maxTokens"));
|
|
2319
2308
|
const retryText = textOf(retry);
|
|
2320
2309
|
if (readReplyJson(retryText).value)
|
|
2321
2310
|
return retryText;
|
|
@@ -2418,19 +2407,12 @@ function checkAbandonmentTriggers(state, thresholds) {
|
|
|
2418
2407
|
message: "Too many choices... I can't think straight anymore. Maybe later.",
|
|
2419
2408
|
};
|
|
2420
2409
|
}
|
|
2421
|
-
// Loop detection
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
if (recentPages.length >= 5 &&
|
|
2425
|
-
uniqueRecent <= thresholds.loopDetectionThreshold) {
|
|
2426
|
-
return {
|
|
2427
|
-
reason: "loop",
|
|
2428
|
-
message: "I keep ending up on the same pages. Something is wrong.",
|
|
2429
|
-
};
|
|
2430
|
-
}
|
|
2410
|
+
// (Loop detection stood here until 21.0: abandon when the last 5 pages
|
|
2411
|
+
// held 3 or fewer distinct ones. pagesVisited records each page once, so
|
|
2412
|
+
// the last 5 were always 5 distinct pages and it could never fire.)
|
|
2431
2413
|
// No progress
|
|
2432
2414
|
if (state.stepCount > thresholds.maxStepsWithoutProgress &&
|
|
2433
|
-
state.goalProgress <
|
|
2415
|
+
state.goalProgress < param("journey.progress.madeLevel")) {
|
|
2434
2416
|
return {
|
|
2435
2417
|
reason: "no_progress",
|
|
2436
2418
|
message: "I'm not making any progress. This isn't working.",
|
|
@@ -2440,30 +2422,21 @@ function checkAbandonmentTriggers(state, thresholds) {
|
|
|
2440
2422
|
if (state.emotionalState) {
|
|
2441
2423
|
const emotional = state.emotionalState;
|
|
2442
2424
|
// High anxiety + high frustration — only abandon if BOTH are extreme AND patience is depleted
|
|
2443
|
-
if (emotional.anxiety >
|
|
2425
|
+
if (emotional.anxiety > param("journey.emotionalAbandon.anxiety") && emotional.frustration > param("journey.emotionalAbandon.frustration") && state.patienceRemaining < param("journey.emotionalAbandon.patience")) {
|
|
2444
2426
|
return {
|
|
2445
2427
|
reason: "emotional",
|
|
2446
2428
|
message: "This is too stressful. I need to take a break.",
|
|
2447
2429
|
};
|
|
2448
2430
|
}
|
|
2449
|
-
//
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
};
|
|
2455
|
-
}
|
|
2456
|
-
// Overwhelming negative valence — only with sustained high arousal AND low patience
|
|
2457
|
-
if (emotional.valence < -0.85 && emotional.arousal > 0.85 && state.patienceRemaining < 0.4) {
|
|
2458
|
-
return {
|
|
2459
|
-
reason: "emotional",
|
|
2460
|
-
message: "I can't deal with this right now.",
|
|
2461
|
-
};
|
|
2462
|
-
}
|
|
2431
|
+
// Two rules stood here until 21.0 that no journey can reach: extreme
|
|
2432
|
+
// boredom (boredom > 0.9), when no trigger the journey fires raises
|
|
2433
|
+
// boredom above its starting value of at most 0.1; and very negative
|
|
2434
|
+
// valence (< -0.85), when valence is an average of the emotions'
|
|
2435
|
+
// valences and the most negative of those is -0.8.
|
|
2463
2436
|
// Apply emotional abandonment modifier to patience threshold
|
|
2464
2437
|
const emotionalModifier = calculateAbandonmentModifier(emotional);
|
|
2465
2438
|
const adjustedPatienceThreshold = thresholds.patienceMin * emotionalModifier;
|
|
2466
|
-
if (state.patienceRemaining < adjustedPatienceThreshold && emotionalModifier >
|
|
2439
|
+
if (state.patienceRemaining < adjustedPatienceThreshold && emotionalModifier > param("journey.emotionalAbandon.modifier")) {
|
|
2467
2440
|
return {
|
|
2468
2441
|
reason: "emotional",
|
|
2469
2442
|
message: "I don't have the energy for this anymore.",
|
|
@@ -2495,27 +2468,119 @@ function ordinalSuffix(n) {
|
|
|
2495
2468
|
default: return "th";
|
|
2496
2469
|
}
|
|
2497
2470
|
}
|
|
2498
|
-
|
|
2471
|
+
/**
|
|
2472
|
+
* The listed clickable a text names. Prefer (1) an exact match, then (2)
|
|
2473
|
+
* starts-with, then (3) includes, each scored less its distance from the
|
|
2474
|
+
* viewport's middle (journey.clickMatch.*); none unless the best score is
|
|
2475
|
+
* positive, so a match further away than its own score is never chosen.
|
|
2476
|
+
*/
|
|
2477
|
+
function matchClickable(elements, text, viewportHeight) {
|
|
2478
|
+
const wanted = text.trim().toLowerCase();
|
|
2479
|
+
const vc = viewportHeight / 2;
|
|
2480
|
+
const candidates = elements.filter(e => e.text);
|
|
2481
|
+
const score = (e) => {
|
|
2482
|
+
const t = (e.text || "").toLowerCase().trim();
|
|
2483
|
+
if (!t)
|
|
2484
|
+
return -Infinity;
|
|
2485
|
+
let base = 0;
|
|
2486
|
+
if (t === wanted)
|
|
2487
|
+
base = param("journey.clickMatch.exact");
|
|
2488
|
+
else if (t.startsWith(wanted) || wanted.startsWith(t))
|
|
2489
|
+
base = param("journey.clickMatch.prefix");
|
|
2490
|
+
else if (t.includes(wanted) || wanted.includes(t))
|
|
2491
|
+
base = param("journey.clickMatch.contains");
|
|
2492
|
+
else
|
|
2493
|
+
return -Infinity;
|
|
2494
|
+
const ey = (e.y ?? 0) + (e.height ?? 0) / 2;
|
|
2495
|
+
const dist = Math.abs(ey - vc);
|
|
2496
|
+
return base - dist;
|
|
2497
|
+
};
|
|
2498
|
+
let best;
|
|
2499
|
+
let bestScore = -Infinity;
|
|
2500
|
+
for (const e of candidates) {
|
|
2501
|
+
const s = score(e);
|
|
2502
|
+
if (s > bestScore) {
|
|
2503
|
+
bestScore = s;
|
|
2504
|
+
best = e;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
return best && bestScore > 0 ? best : undefined;
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* The Fitts geometry of a listed element, as Stage B measures pointing: the
|
|
2511
|
+
* distance from the centre of the screen it is seen on (the pointer's assumed
|
|
2512
|
+
* start) to the element's centre, and the smaller of its width and height.
|
|
2513
|
+
* The box is in viewport coordinates, taken with the page scrolled to
|
|
2514
|
+
* `scrollY`. An element whose centre is on the current screen is seen there;
|
|
2515
|
+
* one above or below is seen once scrolled to, on the screen-sized band of
|
|
2516
|
+
* the page that holds its centre (Stage B's full-page tiling). Undefined when
|
|
2517
|
+
* the element has no usable box.
|
|
2518
|
+
*/
|
|
2519
|
+
function boxGeometry(el, viewport, scrollY) {
|
|
2520
|
+
if (!el || typeof el.x !== "number" || typeof el.y !== "number" || typeof el.width !== "number" || typeof el.height !== "number")
|
|
2521
|
+
return undefined;
|
|
2522
|
+
if (!(viewport.width > 0 && viewport.height > 0 && el.width >= 0 && el.height >= 0 && Number.isFinite(scrollY)))
|
|
2523
|
+
return undefined;
|
|
2524
|
+
const centreY = el.y + el.height / 2;
|
|
2525
|
+
const onScreen = centreY >= 0 && centreY <= viewport.height;
|
|
2526
|
+
const box = { x: el.x, y: el.y + scrollY, width: el.width, height: el.height };
|
|
2527
|
+
return {
|
|
2528
|
+
distance: distanceToScreenCentre(box, { viewportWidth: viewport.width, viewportHeight: viewport.height, scrollY }, onScreen ? "viewport" : "full_page"),
|
|
2529
|
+
width: smallerOfWidth(box.width, box.height),
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
/**
|
|
2533
|
+
* The Fitts geometry of the element a click, hover or hover-click names: by
|
|
2534
|
+
* index (#N), by selector, or by text (matchClickable). Undefined for an
|
|
2535
|
+
* action that moves no pointer, or one whose element is not listed; executeAction
|
|
2536
|
+
* then charges the registered fallback move (journey.fitts.unlocated*).
|
|
2537
|
+
*/
|
|
2538
|
+
function pointingGeometry(action, elements, viewport, scrollY) {
|
|
2539
|
+
const [type, ...args] = action.split(":");
|
|
2540
|
+
if (!["click", "hover", "hoverclick"].includes(type))
|
|
2541
|
+
return undefined;
|
|
2542
|
+
const name = type === "hoverclick" ? (args[0] ?? "") : args.join(":");
|
|
2543
|
+
const index = /^#(\d+)$/.exec(name);
|
|
2544
|
+
const el = index
|
|
2545
|
+
? elements[parseInt(index[1], 10)]
|
|
2546
|
+
: elements.find(e => e.selector === name) ?? matchClickable(elements, name, viewport.height);
|
|
2547
|
+
return boxGeometry(el, viewport, scrollY);
|
|
2548
|
+
}
|
|
2549
|
+
/**
|
|
2550
|
+
* The wait before a mouse action reaches its target: Fitts' law (v9.9.0,
|
|
2551
|
+
* Stage B's law) on the target's measured geometry, or the registered
|
|
2552
|
+
* fallback move for one the journey has not found, plus the gaze-to-mouse
|
|
2553
|
+
* lag. Returns the time, and sleeps it unless humanClick will drive the
|
|
2554
|
+
* pointer (`pacedByHumanClick`).
|
|
2555
|
+
*/
|
|
2556
|
+
async function pointingWait(fittsParams, gazeMouseLag, pacedByHumanClick, pointing) {
|
|
2557
|
+
const distance = pointing ? pointing.distance : param("journey.fitts.unlocatedDistancePx");
|
|
2558
|
+
const width = pointing ? pointing.width : param("journey.fitts.unlocatedTargetWidthPx");
|
|
2559
|
+
let movementTimeMs = calculateFittsMovementTime(distance, width, fittsParams);
|
|
2560
|
+
// Add gaze-to-mouse lag (v10.1.0) - WebGazer.js research
|
|
2561
|
+
// Eye movements precede mouse movements by 200-500ms
|
|
2562
|
+
if (gazeMouseLag) {
|
|
2563
|
+
movementTimeMs += gazeMouseLag;
|
|
2564
|
+
}
|
|
2565
|
+
// With human input on, the coordinate click hands the pointer to
|
|
2566
|
+
// humanClick, whose move and pre-click dwell replace this wait, so the two
|
|
2567
|
+
// don't compound (which would make live mode slower than non-live). A
|
|
2568
|
+
// hover, hover-click or click by name goes through Playwright, which has no
|
|
2569
|
+
// human pacing, so it waits this time with human input on or off.
|
|
2570
|
+
if (!pacedByHumanClick) {
|
|
2571
|
+
await sleep(movementTimeMs);
|
|
2572
|
+
}
|
|
2573
|
+
return movementTimeMs;
|
|
2574
|
+
}
|
|
2575
|
+
async function executeAction(browser, action, fittsParams, gazeMouseLag, humanInput, inputTraits, pointing, pointingPaid = false) {
|
|
2499
2576
|
const [type, ...args] = action.split(":");
|
|
2500
|
-
//
|
|
2577
|
+
// Mouse actions wait their pointing time (pointingWait), unless the caller
|
|
2578
|
+
// already paid it: the retry by name after a coordinate click throws.
|
|
2579
|
+
// Playwright drives the pointer here, never humanClick, so the wait is
|
|
2580
|
+
// slept with human input on too.
|
|
2501
2581
|
let movementTimeMs = 0;
|
|
2502
|
-
if (["click", "hover", "hoverclick"].includes(type) && fittsParams) {
|
|
2503
|
-
|
|
2504
|
-
// Average button width ~80px, average screen movement distance ~300px
|
|
2505
|
-
const estimatedDistance = 300;
|
|
2506
|
-
const estimatedTargetWidth = 80;
|
|
2507
|
-
movementTimeMs = calculateFittsMovementTime(estimatedDistance, estimatedTargetWidth, fittsParams);
|
|
2508
|
-
// Add gaze-to-mouse lag (v10.1.0) - WebGazer.js research
|
|
2509
|
-
// Eye movements precede mouse movements by 200-500ms
|
|
2510
|
-
if (gazeMouseLag) {
|
|
2511
|
-
movementTimeMs += gazeMouseLag;
|
|
2512
|
-
}
|
|
2513
|
-
// When humanInput is enabled, humanClick provides its own hover-and-dwell
|
|
2514
|
-
// pacing — skip the pre-action sleep here so they don't compound (which
|
|
2515
|
-
// would make live mode slower than non-live, defeating the purpose).
|
|
2516
|
-
if (!humanInput) {
|
|
2517
|
-
await sleep(movementTimeMs);
|
|
2518
|
-
}
|
|
2582
|
+
if (["click", "hover", "hoverclick"].includes(type) && fittsParams && !pointingPaid) {
|
|
2583
|
+
movementTimeMs = await pointingWait(fittsParams, gazeMouseLag, false, pointing);
|
|
2519
2584
|
}
|
|
2520
2585
|
switch (type) {
|
|
2521
2586
|
case "click": {
|
|
@@ -2527,8 +2592,8 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2527
2592
|
try {
|
|
2528
2593
|
const escapedSel = selector.replace(/"/g, '\\"');
|
|
2529
2594
|
const link = page.locator(`a:has-text("${escapedSel}")`).first();
|
|
2530
|
-
if (await link.isVisible({ timeout:
|
|
2531
|
-
await link.click({ timeout:
|
|
2595
|
+
if (await link.isVisible({ timeout: param("journey.action.linkVisibleMs") }).catch(() => false)) {
|
|
2596
|
+
await link.click({ timeout: param("journey.action.clickTimeoutMs") });
|
|
2532
2597
|
clicked = true;
|
|
2533
2598
|
}
|
|
2534
2599
|
}
|
|
@@ -2537,8 +2602,8 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2537
2602
|
try {
|
|
2538
2603
|
const escapedSel = selector.replace(/"/g, '\\"');
|
|
2539
2604
|
const btn = page.locator(`button:has-text("${escapedSel}"), [role="button"]:has-text("${escapedSel}")`).first();
|
|
2540
|
-
if (await btn.isVisible({ timeout:
|
|
2541
|
-
await btn.click({ timeout:
|
|
2605
|
+
if (await btn.isVisible({ timeout: param("journey.action.buttonVisibleMs") }).catch(() => false)) {
|
|
2606
|
+
await btn.click({ timeout: param("journey.action.clickTimeoutMs") });
|
|
2542
2607
|
clicked = true;
|
|
2543
2608
|
}
|
|
2544
2609
|
}
|
|
@@ -2550,7 +2615,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2550
2615
|
return { success: false, movementTimeMs };
|
|
2551
2616
|
}
|
|
2552
2617
|
try {
|
|
2553
|
-
await page.waitForTimeout(
|
|
2618
|
+
await page.waitForTimeout(param("journey.action.clickSettleMs"));
|
|
2554
2619
|
}
|
|
2555
2620
|
catch { }
|
|
2556
2621
|
let urlAfter = page.url();
|
|
@@ -2561,12 +2626,12 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2561
2626
|
const escapedSel = selector.replace(/"/g, '\\"');
|
|
2562
2627
|
const href = await page.locator(`a:has-text("${escapedSel}")`).first().getAttribute('href').catch(() => null);
|
|
2563
2628
|
if (href && href.startsWith('http')) {
|
|
2564
|
-
await page.goto(href, { waitUntil: 'domcontentloaded', timeout:
|
|
2629
|
+
await page.goto(href, { waitUntil: 'domcontentloaded', timeout: param("journey.action.gotoTimeoutMs") });
|
|
2565
2630
|
urlAfter = page.url();
|
|
2566
2631
|
}
|
|
2567
2632
|
else if (href && href.startsWith('/')) {
|
|
2568
2633
|
const base = new URL(urlBefore).origin;
|
|
2569
|
-
await page.goto(base + href, { waitUntil: 'domcontentloaded', timeout:
|
|
2634
|
+
await page.goto(base + href, { waitUntil: 'domcontentloaded', timeout: param("journey.action.gotoTimeoutMs") });
|
|
2570
2635
|
urlAfter = page.url();
|
|
2571
2636
|
}
|
|
2572
2637
|
}
|
|
@@ -2601,8 +2666,8 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2601
2666
|
const r = await browser.fill(selector, value);
|
|
2602
2667
|
return { success: r.success };
|
|
2603
2668
|
}
|
|
2604
|
-
await loc.scrollIntoViewIfNeeded({ timeout:
|
|
2605
|
-
await loc.click({ timeout:
|
|
2669
|
+
await loc.scrollIntoViewIfNeeded({ timeout: param("journey.action.focusScrollMs") }).catch(() => { });
|
|
2670
|
+
await loc.click({ timeout: param("journey.action.clickTimeoutMs") });
|
|
2606
2671
|
// Clear if there's existing content
|
|
2607
2672
|
await page.keyboard.press("Control+A").catch(() => { });
|
|
2608
2673
|
await page.keyboard.press("Delete").catch(() => { });
|
|
@@ -2620,7 +2685,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2620
2685
|
// Use age modifier as proxy for typing expertise (younger = faster)
|
|
2621
2686
|
const expertise = fittsParams?.ageModifier
|
|
2622
2687
|
? Math.max(0, 1 - (fittsParams.ageModifier - 1))
|
|
2623
|
-
:
|
|
2688
|
+
: param("journey.typing.defaultExpertise");
|
|
2624
2689
|
const typingTimeMs = calculateTypingTime(value, expertise, true);
|
|
2625
2690
|
// Add realistic typing delay
|
|
2626
2691
|
await sleep(typingTimeMs);
|
|
@@ -2639,10 +2704,11 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2639
2704
|
const page = await browser.getPage();
|
|
2640
2705
|
try {
|
|
2641
2706
|
const beforeY = await page.evaluate(() => window.scrollY);
|
|
2707
|
+
const step = param("journey.action.scrollPx");
|
|
2642
2708
|
if (humanInput && (direction === "down" || direction === "up")) {
|
|
2643
2709
|
// Human-realistic scroll: variable delta + occasional glance-back.
|
|
2644
2710
|
const { humanScroll } = await import("../visual/human-input.js");
|
|
2645
|
-
const baseDelta = direction === "down" ?
|
|
2711
|
+
const baseDelta = direction === "down" ? step : -step;
|
|
2646
2712
|
await humanScroll(page, baseDelta, inputTraits);
|
|
2647
2713
|
}
|
|
2648
2714
|
else {
|
|
@@ -2654,20 +2720,20 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2654
2720
|
await page.evaluate(() => window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }));
|
|
2655
2721
|
break;
|
|
2656
2722
|
case "up":
|
|
2657
|
-
await page.evaluate(() => window.scrollBy({ top: -
|
|
2723
|
+
await page.evaluate((d) => window.scrollBy({ top: -d, behavior: "smooth" }), step);
|
|
2658
2724
|
break;
|
|
2659
2725
|
case "down":
|
|
2660
2726
|
default:
|
|
2661
|
-
await page.evaluate(() => window.scrollBy({ top:
|
|
2727
|
+
await page.evaluate((d) => window.scrollBy({ top: d, behavior: "smooth" }), step);
|
|
2662
2728
|
break;
|
|
2663
2729
|
}
|
|
2664
|
-
await sleep(
|
|
2730
|
+
await sleep(param("journey.action.scrollSettleMs"));
|
|
2665
2731
|
}
|
|
2666
2732
|
// Report success only if the page actually moved. A no-op scroll
|
|
2667
2733
|
// (already at top/bottom, or scroll blocked) returns failure so the
|
|
2668
2734
|
// AI knows to try something else instead of looping scroll:up/down.
|
|
2669
2735
|
const afterY = await page.evaluate(() => window.scrollY);
|
|
2670
|
-
const moved = Math.abs(afterY - beforeY) >
|
|
2736
|
+
const moved = Math.abs(afterY - beforeY) > param("journey.action.scrollMovedPx");
|
|
2671
2737
|
return { success: moved };
|
|
2672
2738
|
}
|
|
2673
2739
|
catch {
|
|
@@ -2678,8 +2744,8 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2678
2744
|
// Browser back button — most common recovery action
|
|
2679
2745
|
const page = await browser.getPage();
|
|
2680
2746
|
try {
|
|
2681
|
-
await page.goBack({ waitUntil: "domcontentloaded", timeout:
|
|
2682
|
-
await sleep(
|
|
2747
|
+
await page.goBack({ waitUntil: "domcontentloaded", timeout: param("journey.action.gotoTimeoutMs") });
|
|
2748
|
+
await sleep(param("journey.action.backSettleMs"));
|
|
2683
2749
|
return { success: true, newUrl: page.url() };
|
|
2684
2750
|
}
|
|
2685
2751
|
catch {
|
|
@@ -2706,7 +2772,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2706
2772
|
}
|
|
2707
2773
|
return false;
|
|
2708
2774
|
}, query);
|
|
2709
|
-
await sleep(
|
|
2775
|
+
await sleep(param("journey.action.findSettleMs"));
|
|
2710
2776
|
return { success: found };
|
|
2711
2777
|
}
|
|
2712
2778
|
catch {
|
|
@@ -2718,9 +2784,9 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2718
2784
|
const page = await browser.getPage();
|
|
2719
2785
|
const count = parseInt(args[0] || "1", 10);
|
|
2720
2786
|
try {
|
|
2721
|
-
for (let i = 0; i < Math.min(count,
|
|
2787
|
+
for (let i = 0; i < Math.min(count, param("journey.action.maxTabs")); i++) {
|
|
2722
2788
|
await page.keyboard.press("Tab");
|
|
2723
|
-
await sleep(
|
|
2789
|
+
await sleep(param("journey.action.tabIntervalMs"));
|
|
2724
2790
|
}
|
|
2725
2791
|
return { success: true };
|
|
2726
2792
|
}
|
|
@@ -2733,7 +2799,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2733
2799
|
const page = await browser.getPage();
|
|
2734
2800
|
try {
|
|
2735
2801
|
await page.keyboard.press("Enter");
|
|
2736
|
-
await sleep(
|
|
2802
|
+
await sleep(param("journey.action.navigationWaitMs"));
|
|
2737
2803
|
const newUrl = page.url();
|
|
2738
2804
|
return { success: true, newUrl };
|
|
2739
2805
|
}
|
|
@@ -2757,7 +2823,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2757
2823
|
case "dismiss": {
|
|
2758
2824
|
// Dismiss overlay/popup/modal — close cookie banners, newsletter popups
|
|
2759
2825
|
try {
|
|
2760
|
-
await browser.dismissOverlay({ type: "auto", timeout:
|
|
2826
|
+
await browser.dismissOverlay({ type: "auto", timeout: param("journey.action.dismissTimeoutMs") });
|
|
2761
2827
|
return { success: true };
|
|
2762
2828
|
}
|
|
2763
2829
|
catch {
|
|
@@ -2770,7 +2836,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2770
2836
|
const page = await browser.getPage();
|
|
2771
2837
|
try {
|
|
2772
2838
|
await page.click(`details:has(summary:has-text("${selector}")) summary, [role="tab"]:has-text("${selector}"), button:has-text("${selector}")`);
|
|
2773
|
-
await sleep(
|
|
2839
|
+
await sleep(param("journey.action.expandSettleMs"));
|
|
2774
2840
|
return { success: true };
|
|
2775
2841
|
}
|
|
2776
2842
|
catch {
|
|
@@ -2781,7 +2847,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
|
|
|
2781
2847
|
}
|
|
2782
2848
|
case "wait": {
|
|
2783
2849
|
// Wait for page to load / content to appear
|
|
2784
|
-
await sleep(parseInt(args[0]
|
|
2850
|
+
await sleep(args[0] ? parseInt(args[0], 10) : param("journey.action.waitDefaultMs"));
|
|
2785
2851
|
return { success: true };
|
|
2786
2852
|
}
|
|
2787
2853
|
default:
|