Matrix logo

Salience

Package matrix/cortex/salience computes and persists the per-memory ranking signal. Every memory has a Score record stored at salience/<id> that tracks the factor inputs. The li...

Package matrix/cortex/salience computes and persists the per-memory ranking signal. Every memory has a Score record stored at salience/<id> that tracks the factor inputs. The live ranking value is computed on demand by ColdScoreWith(score, weights, now) using the actor's current learned Weights.

Source file: cortex/salience/salience.go.


Design decisions

Five factors, weighted sum. Recency (R), Access (A), Citations (C), Declared importance (D), and Vector similarity (V). V is only active when a Near query is in progress; without it, the four remaining weights are renormalized to sum to 1.

Cached is a snapshot, not the live ranking. sc.Cached is written at write-time and bumped by the BumpFor* helpers. Authoritative live ranking uses ColdScoreWith(sc, weights, now) with the actor's current learned Weights. Query and Context paths always call ColdScoreWith — not sc.Cached directly.

Salience is NOT in OverallRoot. The salience/<id> and meta/salience_weights keys are derived state. The Rebuild operation drops and re-derives them from the journal. This is intentional — the recency component R(m) depends on wall-clock time, so the same salience cache could not be reproduced byte-identically on replay anyway.


Formula

salience(m) = w1·R(m) + w2·A(m) + w3·C(m) + w4·D(m) + w5·V(m, q)

R(m) = exp(-Δt(now, last_used) / half_life)           // recency; half-life 90d
A(m) = log(1 + access_count) / log(1 + 1000)          // normalized access
C(m) = log(1 + cite_in_successful_plans) / log(1+1000) // normalized citations
D(m) = declared_importance / 10.0                      // 0..1
V(m, q) = cosine(embedding(m), embedding(q))           // 0 when no Near query

When q.near is unset, w5 is 0 and the remaining weights are renormalized by dividing by their sum (0.90 at cold-start weights), equivalent to "redistribute w5 proportionally".

Cold weights (§8.2)

WeightSymbolDefault
RecencyWR0.25
AccessWA0.15
CitationsWC0.30
Declared importanceWD0.20
Vector similarityWV0.10

These weights are the cold start. Per-actor learned weights override them after cortex.Attest calls accumulate enough signal.

Pinned floor

Memories in the Pinned tier (Identity, hard Constraints, active Goals) receive a salience floor of PinnedFloor = 0.7. Applied in Context and Compact after computing the live score.


Score record

type Score struct {
    SchemaVersion uint8
    LastUsed      int64    // UnixNano; updated by Write, Update, UpdateHead, Attest
    Importance    uint8    // DeclaredImportance at last bump; 0..10
    AccessCount   uint64   // incremented by successful Attest
    Citations     uint64   // citation count in successful plans
    Cached        float32  // last computed cold score (snapshot; not live ranking)
    ComputedAt    int64    // UnixNano of last Cached recompute
}

Bump helpers

HelperWhen calledWhat it does
NewForWrite(importance, now)cortex.WriteSeeds a fresh Score at cold-start values
BumpForUpdate(sc, importance, now)cortex.Update, UpdateHeadAdvances LastUsed, refreshes Cached
BumpForCitation(sc, now)cortex.Attest (success)Increments Citations + AccessCount, refreshes Cached
DecrementCitation(sc, now)cortex.Attest (failure, factual_error or wrong_assumption)Decrements Citations (floor 0), refreshes Cached
ZeroForTombstone(sc, now)cortex.TombstoneSets Cached = 0; factor inputs preserved

Per-actor weight learning

After enough Attest calls, the actor's weights drift away from the cold-start defaults to reflect which factors best predict success on this actor's workload.

type Weights struct {
    SchemaVersion uint8
    WR, WA, WC, WD, WV float32  // must sum to 1.0
    UpdatedAt int64
    Updates   uint64
}

Weights are persisted at meta/salience_weights (one key per actor). The Rebuild operation drops and re-derives this key by replaying KindLearnWeights journal entries.

EMA update (§8.3)

const EMARate float32 = 0.05  // α

// Called from cortex.Attest
salience.UpdateWeightsEMA(&weights, postBumpScores, EMARate, decrementOnFailure, now)

The EMA pulls Weights toward the factor profile of the cited memories:

  • Success or non-decrement failure — pulls TOWARD the cited profile.
  • Decrement-reason failure (factual_error, wrong_assumption) — pulls AWAY.

The direction matches the bandit-lite interpretation: success reinforces the weighting that ranked those memories highly; a factual-error failure penalizes it.

alpha=0.05 is intentionally slow — roughly 14 intents to move the weight by half a standard deviation from its initial value.


Live cold score

score := salience.ColdScoreWith(sc, weights, now)

Computes the live salience using the actor's current learned weights and the current wall clock. This is what query.Run and cortex.Context use for ranking and trim decisions.

ColdScore(sc, now) is the version that uses hard-coded cold weights — used as a fallback when no learned weights exist yet.


Read / encode / decode

// Read from store
sc, ok, err := salience.Read(s, memoryID)

// Encode/decode
bytes, err := salience.Encode(&sc)
err = salience.Decode(bytes, &sc)

// Weights
weights, ok, err := salience.ReadWeights(s)
bytes, err := salience.EncodeWeights(&weights)

Modifying salience

What to changeWhere
Formula factors or weightscortex/salience/salience.goColdScore, ColdScoreWith, cold weight constants
Half-life decay constantcortex/salience/salience.go — the time division in R(m)
Pinned floor valuecortex/salience/salience.goPinnedFloor constant
EMA learning ratecortex/salience/salience.goEMARate constant (also stamped in KindLearnWeights journal entries for replay determinism)
Add a new factorExtend Score struct; add corresponding Weights field; bump WeightsSchemaVersion; update ColdScoreWith; update UpdateWeightsEMA