Attention Modeling And GDPR Boundary#
This note documents how Antics should approach Antics Attention modeling without overstating the privacy status of its training data.
Short Version#
The current in-app scorer is transitional, not the final modeling architecture.
The real model should be fit offline and then distilled into a deterministic production scorer.
The production artifact should come from a two-stage offline pipeline:
CmdStan for posterior inference over the site-scoped model.
HiGHS for constrained post-processing into the runtime artifact.
The current extension point for extra predictive power is the checked-in GBT residual layer, not Vowpal Wabbit.
Vowpal Wabbit fits contextual-bandit routing or policy selection around attention, not the live attention score itself.
Raw pageview training data should be treated as per-site, not pooled across domains by default.
Cross-site help, if added later, should come only from sufficiently anonymised aggregate summaries, not pooled raw event rows.
Why Raw Cross-Domain Pooling Is Not The Default#
Antics is privacy-first, but privacy-first does not mean every event row is automatically anonymous under GDPR.
Current pageview and _leave rows can still carry combinations such as:
page path
timestamp
referrer or source
device/browser context
page-local behavior summaries
Even without names, cookies, or persistent identifiers, these combinations can still leave the data in pseudonymised territory rather than anonymous territory.
The operational rule for Antics should therefore be conservative:
do not assume cross-domain raw pageview rows are anonymous enough to merge wholesale for model fitting
do train offline models per site
if Antics later wants shared priors across sites, derive them only from heavily aggregated, non-linkable summaries
Regulatory Framing#
The relevant distinction is between anonymised and pseudonymised data.
European Data Protection Board:
pseudonymised data remains personal data
properly anonymised data falls outside GDPR
Source:
UK ICO:
pseudonymised data may still be personal data, especially for the party that can still link or single out people
anonymised data must leave people not identifiable by means reasonably likely to be used
Sources:
These sources do not create Antics policy by themselves, but they support the cautious implementation rule above.
Modeling Rule For Antics Attention#
The production metric should follow this shape:
Extract pageview-local evidence from a single site.
Build a latent-label training dataset for that site.
Fit the Bayesian model offline.
Distill the fit into a deterministic production scorer.
Version the scorer and keep it reproducible.
For the first serious fit:
site:
rgoswami.metarget: latent attention fraction of visible time
training labels: balanced latent-label dataset from real pageviews
model family: hierarchical ordered beta
likelihood: ordered beta regression (Kubinec 2023, doi:10.1017/pan.2022.20) with a beta interior (Ferrari and Cribari-Neto 2004, doi:10.1080/0266476042000214501)
hierarchy: page-path random intercepts only
What Counts As Cross-Site Safe Later#
Potentially acceptable later:
aggregate histograms
heavily bucketed parameter summaries
calibration priors that cannot be traced back to a domain, path, or pageview
Not acceptable by default:
pooled raw pageview rows across domains
pooled per-path behavioral traces across domains
anything that assumes “no cookie” automatically means “anonymous”
Implementation Consequence#
When Antics says a model is “Bayesian,” that should mean:
posterior inference happened in the offline fitting pipeline
the live app uses a frozen deterministic scorer derived from that fit
the model version is explicit
the training scope is documented
That keeps the product technically honest and privacy claims defensible.
Concrete Offline Workflow#
The repo now has a concrete offline handoff for fitting a site-scoped model.
1. Export one site’s training evidence#
Export raw pageview-local rows:
cargo run --bin export-attention-training -- \
--duckdb /data/antics.duckdb \
--site-id <site-id> \
--start 2026-01-01 \
--end 2026-03-31 \
--format rows > attention-rows.json
By default this export is real-site-only. If a site has no usable non-bot _leave.attention rows yet, the exporter returns an empty corpus instead of silently mixing in bootstrap data.
If you explicitly want the deterministic bootstrap corpus for fitting experiments or dry runs, add --include-synthetic. Synthetic rows are explicitly marked with "synthetic": true.
Export balanced latent labels:
cargo run --bin export-attention-training -- \
--duckdb /data/antics.duckdb \
--site-id <site-id> \
--start 2026-01-01 \
--end 2026-03-31 \
--include-synthetic \
--format latent > attention-latent.json
Export two files:
a metadata-rich
stanJSON for distillation and audita numeric-only
cmdstanJSON for the sampler itself
cargo run --bin export-attention-training -- \
--duckdb /data/antics.duckdb \
--site-id <site-id> \
--start 2026-01-01 \
--end 2026-03-31 \
--include-synthetic \
--format stan > attention-stan.json
cargo run --bin export-attention-training -- \
--duckdb /data/antics.duckdb \
--site-id <site-id> \
--start 2026-01-01 \
--end 2026-03-31 \
--include-synthetic \
--format cmdstan > attention-cmdstan.json
2. Fit the hierarchical ordered-beta model with CmdStan#
The first checked-in model lives at:
models/attention_fraction_hierarchical.stan
Typical CmdStan flow:
make models/attention_fraction_hierarchical
./models/attention_fraction_hierarchical method=sample \
data file=attention-cmdstan.json \
output file=attention-fit.csv
If posterior checks or deterministic summaries need a second pass, use CmdStan’s standalone generated quantities flow against the fitted CSV.
Once you have a CmdStan draws CSV, distill it into the production artifact JSON with:
posterior draws remain the Bayesian source of truth
HiGHS projects those draws onto a production-safe coefficient surface
the emitted JSON artifact is the single runtime source of truth
cargo run --bin distill-attention-fit -- \
--site-id <site-id> \
--site-domain <site-domain> \
--model-version attention-<site>-v1 \
--stan-json attention-stan.json \
--cmdstan-csv attention-fit.csv \
--output <site-id>.json
3. Distill the fit into a frozen production artifact#
Production does not run MCMC or Stan.
Instead, the offline fit must be converted into a site-scoped JSON artifact with:
schema_versionmodel_versionsite_idsite_domainfeature_ordercoefficientscoefficient_sdspath_interceptspath_intercept_sdscalibrationprovenanceoptional
cutpointsoptional
phioptional
gbt_residual_pathoptional
gbt_residual_alphaoptional
gbt_residual_summary
The distillation step should not be a naive “posterior means into JSON” pass. The intended production path is:
read posterior draws from CmdStan
compute posterior summaries and uncertainty metadata
run HiGHS as a constrained projection step
emit one frozen runtime artifact
That projection step is where Antics can enforce production-safe structure such as:
monotone positive effects for engagement signals like scroll, click, keydown, pointer, and copy
monotone negative effects for idle-heavy signals like
idle_ratioandmax_idle_sbounded coefficients and path intercepts derived from the posterior support
provenance for how much the final runtime artifact had to move away from the posterior target
The live service loads these artifacts from:
ANTICS_ATTENTION_MODEL_DIR/<site-id>.json
These artifacts are part of the deployed analytics data plane. Copy, back up, and restore them alongside the DuckDB analytics and redb control snapshots for any site that depends on a fitted model.
If an artifact is present, stats endpoints score pageviews from that frozen model. If no artifact is present yet, Antics uses the transitional in-app scorer. If an artifact is present but fails validation, the request fails loudly instead of silently reverting to the transitional path.
4. Attach the GBT residual layer#
Antics already has the non-linear extension point the attention scorer needs: a pure-Rust gradient-boosted-tree residual model stored alongside the site-scoped JSON artifact.
The residual layer is intentionally narrower than a second independent model:
score each training row with the distilled CmdStan plus HiGHS linear backbone
train the GBT against
target - linear_predictionwrite the GBT JSON atomically next to the artifact
record the relative model path and fit summary in the artifact
leave
gbt_residual_alphaat0.0unless validation shows the combined score beats the linear-only baseline
At runtime, ANTICS_ATTENTION_MODEL_DIR/<site-id>.json remains the single
control point. If the artifact references a GBT path, the service loads it
through the cached attention entry and adds the residual on the eta/linear
scale before the expectation. When the artifact has two finite cutpoints
c0 and c1, the live score is the ordered-beta unconditional
expectation (Kubinec 2023):
eta = linear_dot + alpha * gbt_residual
p_above_zero = logistic(eta - c0)
p_one = logistic(eta - c1)
p_interior = max(p_above_zero - p_one, 0)
E[Y] = p_interior * logistic(eta) + p_one
attention_seconds = clamp(E[Y] * visible_s, 0, visible_s)
Artifacts without cutpoints keep logistic(eta) * visible_s.
This keeps request-time scoring deterministic and local. A missing residual file during artifact rotation falls back to linear-only scoring for that load; artifact schema errors still fail loudly.
The GBT feature order is fixed separately from the linear artifact feature order:
log_visible_sscrollclickkeydownpointercopyidle_ratiomax_idle_s
The training row extractor and runtime feature builder must keep that order identical. Treat the GBT residual artifact as part of the same backup/restore surface as the JSON attention artifact.
5. Do not use VW for the current attention scorer#
OokCite’s Vowpal Wabbit work is useful as an operational pattern, but it is not the right scoring primitive for the current Antics Attention metric.
Reuse these ideas from the OokCite VW path:
train outside the API request path
make model artifacts explicit and versioned
keep request-time serving bounded
treat malformed policy output as a fault, not as a silent quality downgrade
keep a deterministic fallback when a policy daemon is unavailable
Do not reuse the current OokCite VW code directly inside the attention scorer:
Antics Attention is a calibrated regression over pageview evidence, not a contextual-bandit action choice.
The current runtime score is already deterministic, auditable, and local.
Adding a VW daemon to the score path would add native/service complexity without replacing the CmdStan posterior, HiGHS projection, or GBT residual.
A VW-shaped Antics feature belongs in a policy layer around attention, for example:
choosing which model family or artifact tier to serve for a site
deciding whether a site is ready for residual promotion
selecting sampling, refit, or alert-inspection actions under a logged policy
ranking workspace jump targets if that UI becomes policy-driven
That policy layer should follow the VW contract used elsewhere: offline ADF
examples, a saved model loaded with VW’s normal -i/-t serving shape, and no
request-path subprocess execution.
6. Track fit state in redb#
The redb control store keeps per-site model fit status. It uses explicit states so the scheduler can distinguish:
fallbackeligiblequeuedfittedstalerefit_due
The scheduler policy is tiered:
paid sites become due every 6 hours
free sites use a 30-day cadence
free sites that have gone idle past the monthly window stay
staleinstead of being refit immediately
The admin registry is the only write path for fit state. The worker
uses ANTICS_ADMIN_URL and ADMIN_TOKEN; operators can call the same
routes:
curl -sS -H "Authorization: Bearer $ADMIN_TOKEN" \
"$ANTICS_ADMIN_URL/api/v1/admin/registry/queue?limit=25"
curl -sS -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model_version":"<model-version>","fitted_at":"<rfc3339>"}' \
"$ANTICS_ADMIN_URL/api/v1/admin/registry/<site-id>/mark-fitted"
curl -sS -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"error":"cmdstan failed","fitted_at":"<rfc3339>"}' \
"$ANTICS_ADMIN_URL/api/v1/admin/registry/<site-id>/mark-failed"
That keeps the refresh policy explicit without a second process opening the control store.
7. Run the fit worker#
Deployed, not retired. Production still runs
antics_attention_fit_worker (Quadlet + image
ghcr.io/turtletech-ehf/antics-attention-fit-worker). A failed unit after a
DuckDB snapshot outage is an ops restart, not a product retirement: golive antics ships the worker as a dependent image consumer, and attention scores
need periodic offline fits. After recovering the main analytics store, start
the unit and confirm it is active and writing under
/data/attention-models/ and /data/fit-debug/.
The registry CLI manages fit state. The dedicated worker performs the fitting
loop. Its default registry backend calls the protected Antics admin API, so
provide ANTICS_ADMIN_URL and ADMIN_TOKEN through the service’s secret
environment. Do not place the admin token in a unit file or source repository.
ANTICS_ADMIN_URL=http://127.0.0.1:8480 \
ADMIN_TOKEN='<admin-token>' \
cargo run --bin attention-fit-worker -- \
--duckdb /data/antics.duckdb \
--model-bin /opt/cmdstan/models/attention_fraction_hierarchical \
--model-dir /data/attention-models \
--work-dir /tmp/antics-attention-fit \
run-once
For continuous background fitting, run the same worker under a timer or long-lived service:
ANTICS_ADMIN_URL=http://127.0.0.1:8480 \
ADMIN_TOKEN='<admin-token>' \
cargo run --bin attention-fit-worker -- \
--duckdb /data/antics.duckdb \
--model-bin /opt/cmdstan/models/attention_fraction_hierarchical \
--model-dir /data/attention-models \
--work-dir /tmp/antics-attention-fit \
run 240
The worker does the real end-to-end promotion:
sync the registry
queue due sites
export the full available non-bot training window for each queued site
run the CmdStan model
distill the posterior with HiGHS constraints
atomically install
ANTICS_ATTENTION_MODEL_DIR/<site-id>.jsonmark the site
fittedorfailed
Operational notes:
paid sites become due every 6 hours in the registry
free sites refresh on the 30-day cadence in the registry
the worker fits on real rows by default; synthetic bootstrap data is opt-in
if a site has no usable
_leave.attentionrows, the worker records a deferred fit and retries it according to the registry policythe worker uses the full site-scoped evidence window, not a short rolling slice, so fitted artifacts can reflect the whole historical corpus available
When a fit fails, the registry now records:
last_attempted_atlast_error
and releases the site back into a sensible state instead of leaving it stuck in
queued forever:
unfitted active sites go back to
eligibleunfitted idle sites go back to
stalepreviously fitted sites go back to
refit_due
8. Reproducibility rule#
Every shipped artifact must be attributable to:
the site it was trained on
the input date range
the Stan model version
the distilled production
model_versionthe GBT residual path and validation summary when the residual is enabled
Retuning is allowed. Silent mutation is not.