Reading, Telemetry & Learning Loop

Part of: Pickatale Master Build Wiki | Version: v1.7 | Last Updated: 2026-04-18


6. Event System

Source: (B)(C) + code audit | Status: Confirmed (events marked UNVERIFIED are planned, not yet emitted)

Session Identity Model [V1-PRODUCTION]

Every reading session uses two separate identifiers. These must not be conflated:

FieldTypeLifetimePurpose
learner_id UUID (permanent) Forever β€” never changes, never deleted Canonical identity of the child across all services, schools, and devices. Owned by Account Center.
device_session_id UUID (short-lived) Single login β†’ logout/timeout/device-reuse Tracks which physical device is currently active. Issued on PIN login. Closed on logout, 30-min inactivity, or device reuse by another child.

Rule: All telemetry events include both learner_id and device_session_id. The server resolves device_session_id β†’ learner_id via the device_sessions table. The client never stores or transmits raw learner_id β€” it uses the session JWT which the server resolves server-side.

device_sessions table

CREATE TABLE device_sessions (
  id INT PRIMARY KEY AUTO_INCREMENT,
  device_session_id VARCHAR(36) NOT NULL UNIQUE,  -- UUID, issued on PIN login
  learner_id VARCHAR(36) NOT NULL,                -- FK students.learner_id (permanent)
  created_at DATETIME NOT NULL DEFAULT NOW(),
  last_active DATETIME NOT NULL DEFAULT NOW(),
  expired_at DATETIME NULL,
  end_reason ENUM('logout', 'timeout', 'device_reuse', 'force_abandon') NULL,
  policy_applied ENUM('shared', 'personal') NOT NULL DEFAULT 'shared',
  policy_source ENUM('login_type', 'school_policy', 'device_signal') NOT NULL DEFAULT 'login_type',
  INDEX(learner_id), INDEX(device_session_id), INDEX(expired_at)
);

Session lifecycle

  1. Child enters username + PIN β†’ POST /api/auth/child-login β†’ server issues JWT containing device_session_id (new UUID) + resolves learner_id internally β†’ INSERT device_sessions row.
  2. All subsequent telemetry events carry the JWT. Server extracts device_session_id and resolves to learner_id before storing.
  3. Inactivity timeout (30 min default, school-configurable 10–60 min): UPDATE device_sessions.expired_at=NOW(), end_reason='timeout'. Partial-session threshold check: if β‰₯3 pages AND β‰₯50 words read β†’ save miles + progress + bot signal; else discard.
  4. Device reuse (another child logs in on same device): previous session closed transactionally before new device_session_id issued.

Event Transport

Confirmed Events (emitted today)

All events include both device_session_id (short-lived, device-scoped) and learner_id (permanent child UUID). See Session Identity Model above.

ReadingStarted

{
  "event_id": "uuid",
  "event_type": "book_opened",
  "learner_id": "uuid",
  "device_session_id": "uuid",
  "book_id": "14760-1",
  "client_ts": 1713470400000
}

PageTurned

{
  "event_id": "uuid",
  "event_type": "page_turned",
  "learner_id": "uuid",
  "device_session_id": "uuid",
  "book_id": "14760-1",
  "page_number": 3,
  "time_on_page_ms": 42000,
  "client_ts": 1713470400000
}

WordTapped

{
  "event_id": "uuid",
  "event_type": "word_tapped",
  "learner_id": "uuid",
  "device_session_id": "uuid",
  "book_id": "14760-1",
  "page_number": 3,
  "word": "magnificent",
  "client_ts": 1713470400000
}

ReadingCompleted (session_ended)

{
  "event_id": "uuid",
  "event_type": "session_ended",
  "learner_id": "uuid",
  "device_session_id": "uuid",
  "book_id": "14760-1",
  "client_ts": 1713470400000
}

Side effect: Triggers POST to Learner Bot vocab-taps + session-summary

BookAbandoned

{
  "event_id": "uuid",
  "event_type": "book_abandoned",
  "learner_id": "uuid",
  "device_session_id": "uuid",
  "book_id": "14760-1",
  "page_number": 5,
  "client_ts": 1713470400000
}

QuizAnswered

{
  "event_id": "uuid",
  "event_type": "quiz_answered",
  "learner_id": "uuid",
  "device_session_id": "uuid",
  "book_id": "14760-1",
  "score_pct": 80.0,
  "level_recommendation": "hold",
  "client_ts": 1713470400000
}

System-Level Events (internal, not client-emitted)

LowScoreDetected

Trigger: Bot detects avg quiz score <65% on 2+ consecutive quizzes Emitted by: Learner Bot nightly run Payload:

{
  "event_type": "LowScoreDetected",
  "learner_id": "uuid",
  "avg_score": 55.0,
  "quiz_count": 3,
  "objective_id": 42,
  "timestamp": "2026-04-18T04:00:00Z"
}

Action: Learner state β†’ struggling; Teacher Portal attention list updated

InterventionTriggered

Trigger: Learner state reaches flagged (no sessions 4+ days, 0 books, low quiz avg) Emitted by: Intervention Engine Payload:

{
  "event_type": "InterventionTriggered",
  "learner_id": "uuid",
  "reason": "no_sessions_4_days | zero_books | low_quiz_avg",
  "teacher_id": "uuid",
  "timestamp": "2026-04-18T04:00:00Z"
}

Action: Teacher Portal attention list updated with reason

ObjectiveMastered

Trigger: Assessment Bot returns level_recommendation = "raise" Emitted by: Orchestrator Payload:

{
  "event_type": "ObjectiveMastered",
  "learner_id": "uuid",
  "objective_id": 42,
  "book_id": "14760-1",
  "score_pct": 88.0,
  "new_reading_level": 4.0,
  "timestamp": "2026-04-18T04:00:00Z"
}

Action: Learner Profile reading_level updated; Bot advances to next objective

NightlyBotCompleted

Trigger: Learner Bot nightly run finishes for a learner Emitted by: Learner Bot Payload:

{
  "event_type": "NightlyBotCompleted",
  "learner_id": "uuid",
  "teacher_report_generated": true,
  "parent_digest_generated": true,
  "gaps_detected": 3,
  "timestamp": "2026-04-18T04:00:00Z"
}


7. Core Modules

Source: (A)(B)(C) + code audit 2026-04-18 | Status: Confirmed per module (exceptions noted)

7.1 Reader App

Features β€” Confirmed:

Features β€” NOT YET BUILT:


7.2 Account Center (SSO Hub)

SSO mechanics: Sets uc_session cookie on .readingtester.com domain. All services verify via GET /api/auth/session. Admin creation: CLI only.

Missing: learner_profiles schema; change/reset password; email verification; account deletion (GDPR).


7.3 Telemetry Service

Confirmed events: book_opened, page_turned, word_tapped, session_ended, quiz_answered, book_abandoned Deduplication: event_id (UUID), 7-day retention window On session_end: fires vocab-taps + session-summary to Learner Bot (5s timeout, fire-and-forget) Privacy: Raw touch data must be anonymised after session analysis. Voice audio MUST NOT be stored. Audio may exist only in-memory during request processing and must be deleted immediately after transcription. No logging, caching, queues, or retries may contain raw audio.


7.4 LRS

Permanent audit-grade record of all learning events. GDPR export/delete endpoints confirmed.


7.5 Learner Profile Service

Key fields: learner_id (UUID PK), reading_level (DECIMAL 4,2), reading_level_history (JSON), wcpm_history (JSON), cefr_level (VARCHAR), interest_tags (JSON), curriculum_territory (VARCHAR), age (INT), language (VARCHAR).

Note: no per-student consent fields. School DPA covers all enrolled students. See Admin & Compliance.

Data writers confirmed: Fluency Assessment β†’ pushes FK + CEFR. Analytics model updater (02:30 UTC) β†’ updates reading_level. Onboarding β†’ sets interest_tags. Voice Assessment β†’ must push FK (NOT YET WIRED).


7.6 Learner Bot

Cron: 04:00 UTC nightly run. 03:00 UTC telemetry retention cleanup. Auth: All routes: X-Internal-Key required. Confirmed endpoints: 20 total (see API Contracts section 4.3).


7.7 Adaptive Content Engine

ZPD offset: child FK level + 0.5 (slightly above current level). Known issue: FK scores sometimes unchanged in practice. GPT prompt tightening required.


7.8 Curriculum Mapper (CM)

Extraction workers: spawn as detached child processes (survive server reloads). Critical gap: Orchestrator cannot query CM without REST API. Must build GET /api/v1/objectives.


7.9 Content Service + Recommendation Engine

Recommendation phases:



7.11 Analytics + ETL + Model Updater

Cron: 02:30 UTC model updater (updates reading_level in Learner Profile). 03:30 UTC GDPR retention.


7.12 Intervention Engine

Flags: No placement test completed | 0 books read | avg quiz <65% on 2+ quizzes | 0 sessions in 4+ days. Output: Attention list entries in Teacher Portal.


7.13 Teacher Portal

Known bug: Class name display mismatch β€” input "Year3Blue" displays as "Year 3 Red" (CLASS ID 4). No transformation code found. Root cause unknown.

Student login cards: name + username + 4-digit PIN (shown once only, not recoverable) + QR code β†’ app.readingtester.com?user=username.


7.14 Parent Portal

Confirmed endpoints: GET /api/v1/digest/preview/:child_id Β· GET /api/v1/gdpr/export/:id


7.15 Fluency Assessment

Scoring: Whisper transcribes β†’ GPT-4o scores WCPM + accuracy + automaticity + error breakdown. FK grade + CEFR calculated from WCPM + accuracy + year level. Multilingual learner flag: accuracy_too_low threshold relaxed to 40%, wcpm_too_low to 5 WCPM.


7.16 Services Returning 502 (Must Fix β€” Phase 3)

Service Domain Code Location Action
Voice Assessment voice.readingtester.com /home/ubuntu/Voice/ Start service; wire to Learner Profile + Telemetry
Decodables decodable.readingtester.com disk (Flask + React) Start service; expose API for Orchestrator
Talk AI talk.readingtester.com β€” Start service; wire to Learner Profile + Telemetry


42. Event Processing Rules

Source: (D) | Status: Confirmed as spec

42.1 Event Types and Producers

Event Producer Consumer Delivery
page_turned Reader App (client) Telemetry Service Fire-and-forget HTTP POST
word_tapped Reader App (client) Telemetry Service Fire-and-forget HTTP POST
session_ended Reader App (client/SW) Telemetry Service HTTP POST (retried offline)
vocab-taps Telemetry Learner Bot HTTP POST, 5s timeout, fire-and-forget
session-summary Telemetry Learner Bot HTTP POST, 5s timeout, fire-and-forget
quiz-result Reader App Learner Bot HTTP POST, 5s timeout, fire-and-forget
fluency-result Fluency Assessment Learner Bot HTTP POST, 5s timeout, fire-and-forget
entitlement-invalidate Account Center All services HTTP POST broadcast, fire-and-forget
nightly-bot-run Cron (04:00 UTC) Learner Bot Internal trigger

42.2 Retry Logic

Client β†’ Telemetry (offline):

Telemetry β†’ Learner Bot:

Nightly bot run:

42.3 Deduplication

Client events: event_id (UUID) is client-generated. Telemetry inserts with UNIQUE constraint on event_id. Duplicate β†’ silently ignored (return 200, not 409 β€” client does not need to know).

Idempotency keys: See Section 39.4 for create-resource deduplication.

Nightly bot: Each learner processed once per night. Check nightly_reports.generated_at > date(now()) before processing. If already generated today β†’ skip.

42.4 Ordering Guarantees

No ordering guarantee between Telemetry events from different client sessions. Events from the same device_session_id are ordered by client_ts (client timestamp). Server must order by client_ts, not created_at, when reconstructing session timeline.

session_ended always processed after page_turned / word_tapped for the same session. Enforced by: session-summary query runs on session_ended trigger β€” any events arriving after session_ended with same device_session_id are still captured (Telemetry inserts them normally; bot has already processed but next nightly cycle will catch the gap).

42.5 LRS Reconciliation

Rule: LRS receives a copy of every completed reading session as an xAPI statement. NOT real-time β€” reconciliation runs on session_ended.

Flow:

Reader App session_ended β†’ Telemetry
                         β†’ (also) POST /api/statements (LRS, xAPI format)
                              Actor: learner_id
                              Verb: http://adlnet.gov/expapi/verbs/completed
                              Object: book_id
                              Result: {score: quiz_score, duration: session_duration}

LRS is write-once for completed sessions. No updates. No deletes (except GDPR erasure).



43. Job / Queue Architecture

Source: (D) | Status: Confirmed as spec. v1 uses in-process scheduling. Queue system is recommended default for v2.**

43.1 Queue System (v1)

v1 uses node-cron (already installed in learner-bot, telemetry) for scheduled jobs. No external queue (no Bull, no RabbitMQ) in v1. Jobs run in-process.

Upgrade path (v2): Bull + Redis for all async jobs. Do NOT build with Bull in v1 β€” premature. Use in-process cron and HTTP fire-and-forget.

43.2 Job Types

Job Service Schedule Trigger
Telemetry retention Telemetry 03:00 UTC daily cron
Nightly bot run Learner Bot 04:00 UTC daily cron
Weekly parent digest Learner Bot Sunday 08:00 UTC cron
Subscription expiry check Account Center Every hour cron
Session force-abandon Reader App Every 6h cron
Invite expiry Account Center Every 6h cron
LRS reconciliation Reader App 05:00 UTC daily cron (after bot run)

43.3 Job Schema

Each cron job must log its run:

CREATE TABLE cron_runs (
  id           INT PRIMARY KEY AUTO_INCREMENT,
  job_name     VARCHAR(100) NOT NULL,
  started_at   DATETIME NOT NULL,
  completed_at DATETIME NULL,
  state        ENUM('running','completed','failed') NOT NULL DEFAULT 'running',
  items_total  INT NULL,
  items_ok     INT NULL,
  items_failed INT NULL,
  error_log    JSON NULL,
  INDEX(job_name, started_at)
);

43.4 Retry Policy

Job failure type Retry
Full job crash Alert via audit_log. Retry once after 1h. Manual re-trigger via admin endpoint.
Individual item failure (per-learner) Log to error_log JSON in cron_runs. Continue next item. No per-item retry.
DB write failure Retry 3Γ— with 500ms backoff. On 3rd failure: log + skip item.
External API failure (GPT, SendGrid) Retry 2Γ— with exponential backoff (2s, 8s). On failure: log, skip item.

43.5 Dead-Letter Handling

v1 has no formal dead-letter queue. Failed items are logged in cron_runs.error_log. Admin can:

  1. View failures via GET /api/admin/jobs/:job_name/runs (last 10 runs, error details)
  2. Re-trigger failed learners via POST /api/admin/bot/run-learner/:learner_id
  3. Re-trigger full nightly run via POST /api/admin/bot/run-nightly (platform_admin only)