Skip to content
CleverKeys Wiki
implemented v1.5.x User guide

Next-Word Prediction Technical Specification

Overview

Opt-in (default OFF) context-only word suggestions generated from the on-device learned n-gram store (BigramStore + TrigramStore via ContextModel), surfaced in the suggestion bar at moments it would otherwise be empty. Landed 2026-08-06 alongside the persistent context LM, the master on-device-learning privacy gate, and suggestion provenance; a shipped static bigram seed was added 2026-08-28 (ARC-020) to fill the slots learned data cannot on a fresh install. Internal engineering spec: docs/specs/context-learning-and-next-word.md (repo source tree).

Key Components

ComponentFilePurpose
NextWordPredictorsrc/main/kotlin/tribixbite/cleverkeys/NextWordPredictor.ktPure JVM gating (shouldShow) + candidate generation (generate) + provenance note
ContextModelsrc/main/kotlin/tribixbite/cleverkeys/contextaware/ContextModel.ktgetNextWordCandidates — trigram-preferred with bigram backoff
BigramStore / TrigramStoresrc/main/kotlin/tribixbite/cleverkeys/contextaware/Persistent, language-keyed, process-singleton learned n-gram stores
LearningGatesrc/main/kotlin/tribixbite/cleverkeys/LearningGate.ktMaster privacy gate; incognito-field flag handling
SuggestionHandlersrc/main/kotlin/tribixbite/cleverkeys/SuggestionHandler.ktImpure wiring: the four call-sites, executor, bar posting
SuggestionProvenancesrc/main/kotlin/tribixbite/cleverkeys/SuggestionProvenance.ktSuggestionOrigin.NEXT_WORD metas + long-press sheet formatting
StaticBigramSeedsrc/main/kotlin/tribixbite/cleverkeys/StaticBigramSeed.ktPure parse/merge/rank over the shipped assets/bigrams/<lang>_bigrams.json cold-start pairs
BigramModelsrc/main/kotlin/tribixbite/cleverkeys/BigramModel.ktLoads those assets (async) and serves getPredictions(prevWord)

Architecture

committed word
   │  (LearningGate.learnCommittedWord — master gate + per-feature gates + incognito flag)

ContextModel.recordCommit ──▶ BigramStore / TrigramStore   (RAM + debounced persist)

        NextWordPredictor.shouldShow    │ ContextModel.getNextWordCandidates(maxResults=10)
        (7-condition gate)              ▼   trigram (w1,w2) first, bigram backoff, dedup
                └──────────▶ NextWordPredictor.generate
                              floors: freq ≥ 2 AND prob ≥ 0.05
                              filters: self-repetition, dictionary/user-vocab membership,
                                       not disabled, dedup
                              score = prob × (1 + personalizationBoost/4) × 1000

                              THEN, only for slots still empty:
                              BigramModel.getPredictions(last context word)
                              (shipped assets; same filters, no floors, no personalization,
                               scores capped below the learned floor so learned always wins)
                                        │  (≤3 whole-bar; ≤2 appended after swipe alternates)

                          SuggestionBar (NEXT_WORD metas, generation-guarded post)

Gating (NextWordPredictor.shouldShow)

ALL must hold: next_word_prediction_enabledon_device_learning_enabled (master) ∧ field allows personalized learning (EditorInfo.imeOptions lacks IME_FLAG_NO_PERSONALIZED_LEARNING = 0x1000000) ∧ word_prediction_enabled ∧ not password mode ∧ no special prompt active ∧ not Termux ∧ non-empty committed context.

The four call-sites (SuggestionHandler)

#TriggerBehavior
1Word completed with space (text == " " only)Show up to 3 candidates in the otherwise-empty bar
2Manual suggestion tap (isManualSelection only)Chain: regenerate from the grown context
3Swipe auto-insert resultsKeep alternates, APPEND ≤2 NEXT_WORD-tagged candidates; tap on those APPENDS instead of replacing the swipe word; generation runs on the shared prediction executor
4Cursor parked with empty prefix (handleCursorParkPrediction)Reads the text actually before the parked cursor (readEditorParkContextNextWordPredictor.contextFromEditorText, sentence-boundary aware), so parking into an older paragraph predicts from it; the editor read is gated on the feature pref, master learning gate, context-LM pref and the per-field incognito flag, and falls back to session context if the editor cannot be read

Staleness: async posts abort when SuggestionBar.contentGeneration() changed since submit. Dismissal: any selection consumes the state; backspace with no partial word clears the candidates; typing a letter switches to prefix predictions; sentence-final punctuation resets the learned-context window (WordPredictor.onSentenceBoundary()).

Configuration

SettingKeyDefaultValuesSource
Next-Word Predictionnext_word_prediction_enabledfalseboolConfig.kt:555
Learn From My Typing (master)on_device_learning_enabledtrueboolConfig.kt:554
Context Sourcecontext_source"both"both | learned_only | static_onlyConfig.kt:556
Personalization Strengthpersonalization_weight1.00.0–2.0Config.kt:557
Suggestion Origin Markerssuggestion_provenance_markersfalseboolConfig.kt:544

Constants: MAX_SUGGESTIONS = 3, MAX_SWIPE_APPEND = 2, MIN_LEARNED_FREQUENCY = 2, MIN_LEARNED_PROBABILITY = 0.05f (NextWordPredictor.kt).

Provenance

Each candidate carries SuggestionMeta(SuggestionOrigin.NEXT_WORD, note = provenanceNote). The note is a structured ProvenanceNote.NextWord value containing the effective context, frequency, percentage, and static-seed flag; no display-language sentence travels through the prediction pipeline. At long-press time, SuggestionHandler resolves Android resources and ProvenanceFormatter renders the learned-statistics or built-in-continuation template. The origin stays NEXT_WORD for both tiers. The opt-in marker dot uses the same origin metadata through SuggestionBar.originMarkerColor.

Test Coverage

SuiteFileFocus
Pure JVMsrc/test/kotlin/tribixbite/cleverkeys/NextWordPredictorTest.ktGate matrix, floors, filters, ranking, static cold-start tier
Pure JVMsrc/test/kotlin/tribixbite/cleverkeys/StaticBigramSeedTest.ktShipped asset schema, merge policy, fallback index
Pure JVMsrc/test/kotlin/tribixbite/cleverkeys/OnDeviceLearningPrivacyTest.ktMaster-gate-off ⇒ nothing recorded/persisted
Pure JVMsrc/test/kotlin/tribixbite/cleverkeys/contextaware/ContextModelTrigramTest.ktTrigram→bigram backoff
Pure JVMsrc/test/kotlin/tribixbite/cleverkeys/LearningWiringDriftTest.ktForbids ungated learn-path regrowth