@memhtml/domain: the pure math under memhtml’s retrieval and sleep phases: cosine similarity and pairwise neighbor selection, reciprocal-rank fusion and maximal marginal relevance, PageRank and communities over the memory graph, the frame key, the retention scorer with its triage bands and reprieve gate, confidence decay on a fixed-point grid, the reinforcement cooldown, and the near-duplicate merge guards. Every export is synchronous, deterministic and free of I/O, so a full index rebuild or a sleep re-run reproduces the same numbers, and the package’s dist imports nothing but effect; tests/layering.test.ts is the standing proof.
The similarity space starts at cosine, which is total and clamped to [-1, 1]: a zero-magnitude vector, or one carrying a NaN or infinite component, reads 0 rather than NaN, because MMR folds a max over similarities and Math.max(penalty, NaN) disagrees with NaN > penalty. The clamp exists because subnormal magnitudes push the unclamped ratio to 1.000000106821595, mismatched lengths walk the shorter vector, and the argument type is ArrayLike so a Float32Array decoded off a stored blob is accepted as is, since this function is the body of the vector_distance_cos SQL function and runs once per candidate row. cosineDistance is 1 - similarity in [0, 2], the space the vector arm’s SQL works in, and compensatedSum is Neumaier summation, needed because naive addition of the retention weight profiles returns 0.9999999999999999 for two of the six. neighbors.ts is the n-by-n arm of the same space: the SQL boundary is priced per call and re-copies the corpus n times over, so consumers decode each vector once into a KeyedVector and topNeighborPairs computes every unordered pair’s similarity once, offers it to both endpoints’ neighborhoods (so (a, b) and (b, a) can both appear), keeps the per-source top perSourceK above floor, and orders the result sim descending, src ascending, dst ascending before the limit, the three knobs of NeighborOptions. Pair similarities are bit-identical to cosine because the square roots are hoisted but the operations run in the same order. rankCandidatePairs applies the same floor, per-source top-k and ordering to an enumerated CandidatePair set, the shape sharedEntityPairs in packages/sleep/src/sql.ts feeds it with for edge typing’s shared-entity candidates, and float32View views a row’s bytes as a Float32Array in place, copying only when the offset is misaligned and returning undefined for an empty or ragged blob rather than throwing. Each returned NeighborPair carries src, dst and sim.
Fusion is split between the production constants and a reference fold. ranking.ts holds RRF_K (60, the published default and the literal the retrieval SQL inlines), MMR_LAMBDA (0.5) and REINFORCE_COOLDOWN_S (900 seconds), plus rrfContribution, which returns an Option that is None for a zero-weight arm or an out-of-range rank so a disabled arm is absent from the fold rather than a neutral term. rrf.ts is the reference implementation, not the production path: packages/index/src/retrieval-sql.ts inlines the fusion arithmetic in SQL sharing only RRF_K, and this fold is what that SQL is checked against. An ArmHit carries a 1-based rank within its own arm’s list, rrfScore is weight / (rank + k), fuseArms sums contributions into a path -> score map (order-insensitive across arms, and a duplicate path inside one arm accumulates, matching the SQL’s SUM over a UNION ALL), rankFused orders best first with ties broken on path ascending, and fuseLateral folds a graph walk’s best-first order in as one more arm, returning the scores unchanged for an empty order or non-positive weight so the lateral arm is inert when dark; it is defined and tested here and has no production caller yet. applyMmr then reorders MmrCandidate rows greedily by lambda * relevance - (1 - lambda) * maxSimilarityToSelected with no embedding round-trip, since candidates carry their own vectors; at lambda >= 1 it returns the input order truncated, a vectorless candidate takes penalty 0 because it cannot be shown to duplicate anything, and each round folds only the newest selection into a cached per-candidate max, so the cost is O(k times n) dot products rather than O(k squared times n).
graph.ts replaces two networkx calls with deterministic equivalents, because these scores feed the retention pagerank and bridgeImportance signals and a run-to-run reordering would change which memories are evicted on an unchanged corpus: nodes are sorted before iteration, parallel edges fold to their maximum strength, and label propagation uses sorted order with lexicographic tie-breaking instead of a random seed. pagerank is weighted power iteration over GraphEdge rows (strength in [0, 1]) with PAGERANK_DAMPING 0.85 (the networkx default), PAGERANK_MAX_ITERATIONS 100 and PAGERANK_TOLERANCE 1e-6; passing seeds makes it personalized, the form a query-conditioned graph walk would use (the one production caller, the sleep retention pass in packages/sleep/src/retention.ts, passes none), seeds naming absent nodes are dropped, and dangling mass is redistributed over the teleport distribution so the scores sum to 1. labelPropagation reads the edges as undirected, canonicalizes each community to its smallest member path, and collapses communities below MIN_COMMUNITY_SIZE (3) to undefined within LABEL_PROPAGATION_MAX_SWEEPS (100), since a pair passed off as a community would make every cross-pair edge look like a bridge; bridgeCounts then counts each node’s edges that cross a community boundary, with a node in no community counting 0 rather than its full degree. frameKeyOf is the other structural key: it reads a claim as a frame (subject plus relation up to the last linking token among of, is, in, to, by, as) and a value, so “The capital of India is New Delhi” keys on the capital of india is and a later claim writing a different city into the same slot is recognizable without a model or an embedding. It fails closed to null unless the frame is at least three tokens and the value is one to six tokens, is case- and whitespace-insensitive, and is a verbatim port from the evaluation harness’s consolidate adapter, where the rule was measured against the benchmark’s conflict-resolution rows before it was believed; the tokens, thresholds and greedy match are fixed by that measurement, and tests/frame.test.ts carries the reference’s own cases.
retention.ts is the eight-signal scorer ported from the predecessor’s domain/retention.py. scoreRetention takes a RetentionInput of raw per-memory quantities (age in days, access count, confidence, PageRank and the run’s maximum, bridge count, inbound reinforcement count, word count, and authored contradiction count), normalizes them with computeSignals into the Signals named by SIGNAL_NAMES (recency, accessFrequency, confidence, pagerank, bridgeImportance, reinforcementCount, contentDensity, contestedStatus), weights them with compositeScore under the type’s WeightProfile from weightsFor, and bands the result with bandFor into a RetentionScore. WEIGHT_PROFILES has five typed profiles (episodic, semantic, procedural, arc, error_pattern) and every other type takes DEFAULT_WEIGHTS; each profile’s eight weights sum to exactly 1.0 under profileWeightSum, which is what keeps the composite in [0, 1], and SCORE_PRECISION rounds it to 4 places. HALF_LIVES_DAYS gives the recency half-life per type (episodic 10, semantic 90, arc 30, error_pattern 14, procedural and task null for no decay) with DEFAULT_HALF_LIFE_DAYS 30 for an unlisted type, and LN2 is Math.LN2 so the curve is exactly 0.5 at the half-life. The TRIAGE_ACTIONS are keep, compress and evict, with KEEP_THRESHOLD 0.7 and EVICT_THRESHOLD 0.3 and each boundary owned by the lower band, so exactly 0.7 compresses and exactly 0.3 evicts. The reprieve gate sits beside the scorer: reprieveScore sums four terms over a ReprieveInput with REPRIEVE_W_IMPORTANCE 0.4, REPRIEVE_W_ACCESS 0.3, REPRIEVE_W_OUTCOME 0.2 and REPRIEVE_W_RECENCY 0.1, decaying recency at SALIENCE_DECAY_RATE 0.01 per hour; the coefficients sum to 1.0 but the score is not convex because the log1p(accessCount) term is unbounded, and a negative outcome score contributes exactly 0 so a memory is not punished twice for one bad outcome. shouldReprieve is the boolean gate, with no side effect of its own: it is true when the score clears REPRIEVE_FLOOR (0.5) and the memory has been reprieved fewer than MAX_REPRIEVES (3) times, where 0 is the kill switch that restores pure-age TTL, and the sleep reprieve phase (packages/sleep/src/phases/reprieve.ts) is what then extends the memory’s memhtml-valid-until by REPRIEVE_DAYS (14).
decay.ts puts confidence decay and the outcome EWMA on a fixed-point grid of SCALE 10^4 (POS_ONE_FP and NEG_ONE_FP are +1.0 and -1.0 on it, toFp and fromFp convert) so the boundary claims hold exactly rather than within an ulp: ewmaStepFp is one step with floor division, applyOutcomesFp folds signals with one rounding per step so an N-signal batch equals N single calls, and applyNegativeHitsFp decays toward -1.0 over a count of negative corrections and is a no-op for hits <= 0. The confidence half is the production path, called by @memhtml/sleep‘s confidence-decay phase: decayConfidenceFp is an EWMA toward the floor followed by a min with the previous value, so decay is unconditionally non-increasing and never rehabilitates a claim an operator pushed below the floor, decayConfidenceNFp folds it over cycles, and decayConfidence and decayConfidenceN are the [0, 1] float wrappers defaulting to DEFAULT_CONFIDENCE_DECAY_ALPHA (0.1, a tenth of the remaining distance per sleep cycle) and DEFAULT_CONFIDENCE_FLOOR (0.2, because a decayed-to-zero memory would be indistinguishable from a retracted one). The outcome half is the reference implementation: packages/index/src/reinforce.ts computes the EWMA in float SQL with its own OUTCOME_EWMA_ALPHA twin of DEFAULT_EWMA_ALPHA (0.3), and both packages’ tests pin the pair. CONFIDENCE_COMMIT_DELTA (0.005) and isCommittableConfidenceChange drop a sub-threshold change so the widest commit of a sleep run stays reviewable. reinforce.ts is the cooldown predicate whose twin is the salience arm’s SQL guard: shouldBumpAccess is true when no stamp exists or the elapsed time is at least the cooldown (>=, matching the SQL, so a stamp exactly 900 seconds old is bumpable), partitionByCooldown splits paths into bumped and cooledDown in one order-preserving pass, and REINFORCE_SIGNALS (positive, negative, neutral) map through signalValue to 1, -1 and 0, where neutral bumps the access count without moving the outcome score because being read is evidence of relevance, not correctness. merge.ts guards the near-duplicate merge, which keeps the older file and so would fold a newer correction into an older wrong memory if it trusted cosine alone: negationDivergent, numericTokenDivergent and variantQualifierDivergent catch “safe” against “NOT safe”, “retry 3” against “retry 13” and “M1” against “M1 Pro”. Each memory is a MergeText of gist (the one-line claim) and body (the article), and the predicates read different ones: mergeVetoReasons runs the numeric predicate over the gist alone, because a body’s citations (a date, a ticket id, an exit code) differ between two honest tellings of one fact, and runs negation and variant over joinMergeText of both, because a polarity flip stated only in the body still contradicts. mergeVetoed is true when any reason fires. mergeOutcomes classifies oriented MergePair rows into a MergeStage each (cap, threshold, vetoed, self, role-guard, committed) by applying in order the MAX_MERGE_PAIRS cap (100 per sleep cycle), the strict NEAR_DUPLICATE_THRESHOLD (0.92), the veto when both texts are present, a self-merge check, and an in-batch role guard under which a dropped path is never dropped again or kept, and a keeper is never dropped, so a transitive chain cannot supersede content into a file the same batch archives; a keeper may keep again, so one page absorbs several duplicates in one batch. mergeCandidates is the committed subset as MergeDecision rows. The guards are a post-filter over every proposal including a model’s, so binding a model to dedup-merge cannot widen the committable set. connectedComponents is the order-invariant union-find whose components are dedup’s units of work (the smaller root always wins, so the same edge set yields the same partition in any arrival order), and excludeSelfSupersede removes the canonical from a compress batch’s member list so the file just folded into is not archived.
Modules
Section titled “Modules”The package publishes one import path, @memhtml/domain, which re-exports the eleven modules below plus a type-only re-export of InvalidMemory from @memhtml/contracts/errors, so the reference on this page is the whole exported surface.
| Module | What it holds |
|---|---|
cosine.ts |
cosine (total, clamped similarity), cosineDistance (1 - similarity) and compensatedSum (Neumaier summation). |
decay.ts |
The grid (SCALE, POS_ONE_FP, NEG_ONE_FP, toFp, fromFp), the defaults (DEFAULT_EWMA_ALPHA, DEFAULT_CONFIDENCE_FLOOR, DEFAULT_CONFIDENCE_DECAY_ALPHA), the EWMA steps (ewmaStepFp, applyOutcomesFp, applyNegativeHitsFp), confidence decay (decayConfidenceFp, decayConfidenceNFp, decayConfidence, decayConfidenceN), and the commit gate (CONFIDENCE_COMMIT_DELTA, isCommittableConfidenceChange). |
frame.ts |
frameKeyOf, the claim’s slot key or null. |
graph.ts |
GraphEdge, the constants (PAGERANK_DAMPING, PAGERANK_MAX_ITERATIONS, PAGERANK_TOLERANCE, MIN_COMMUNITY_SIZE, LABEL_PROPAGATION_MAX_SWEEPS), pagerank, labelPropagation and bridgeCounts. |
merge.ts |
NEAR_DUPLICATE_THRESHOLD, MAX_MERGE_PAIRS, the shapes MergeText, MergePair, MergeDecision, MergeOutcome and MergeStage, joinMergeText, the guards (negationDivergent, numericTokenDivergent, variantQualifierDivergent, mergeVetoReasons, mergeVetoed), mergeOutcomes, mergeCandidates, connectedComponents and excludeSelfSupersede. |
mmr.ts |
MmrCandidate and applyMmr. |
neighbors.ts |
The shapes KeyedVector, NeighborPair, NeighborOptions and CandidatePair, float32View, topNeighborPairs and rankCandidatePairs. |
ranking.ts |
RRF_K, MMR_LAMBDA, REINFORCE_COOLDOWN_S and rrfContribution. |
reinforce.ts |
shouldBumpAccess, REINFORCE_SIGNALS, ReinforceSignal, signalValue and partitionByCooldown. |
retention.ts |
The signal and profile types (SIGNAL_NAMES, SignalName, Signals, WeightProfile, TRIAGE_ACTIONS, TriageAction), the tables (WEIGHT_PROFILES, DEFAULT_WEIGHTS, HALF_LIVES_DAYS, DEFAULT_HALF_LIFE_DAYS, LN2, KEEP_THRESHOLD, EVICT_THRESHOLD, SCORE_PRECISION), the scorer (RetentionInput, RetentionScore, weightsFor, halfLifeFor, profileWeightSum, computeSignals, compositeScore, bandFor, scoreRetention), and the reprieve gate (REPRIEVE_FLOOR, REPRIEVE_DAYS, MAX_REPRIEVES, SALIENCE_DECAY_RATE, REPRIEVE_W_IMPORTANCE, REPRIEVE_W_ACCESS, REPRIEVE_W_OUTCOME, REPRIEVE_W_RECENCY, ReprieveInput, reprieveScore, shouldReprieve). |
rrf.ts |
ArmHit, rrfScore, fuseArms, rankFused and fuseLateral. |
index.ts |
The eleven re-exports above plus the type-only re-export of InvalidMemory from @memhtml/contracts/errors, erased by verbatimModuleSyntax so dist still imports nothing but effect. |
Interfaces
Section titled “Interfaces”ArmHit
Section titled “ArmHit”One arm’s hit. rank is a 1-based position within that one arm’s own candidate list,
not a global rank and not a score. That is what lets RRF work across arms whose scores
are incomparable.
Properties
Section titled “Properties”readonly path: string;readonly rank: number;CandidatePair
Section titled “CandidatePair”One enumerated candidate pair, oriented by the caller. Pairs must be distinct.
Properties
Section titled “Properties”readonly dst: string;readonly src: string;GraphEdge
Section titled “GraphEdge”One directed weighted edge. strength is unitless in [0, 1].
Properties
Section titled “Properties”readonly dst: string;readonly src: string;strength
Section titled “strength”readonly strength: number;InvalidMemory
Section titled “InvalidMemory”A memory that violates the file format or the type/placement vocabulary.
Extends
Section titled “Extends”InvalidMemory_base
Properties
Section titled “Properties”reason
Section titled “reason”readonly reason: string;Inherited from
Section titled “Inherited from”InvalidMemory_base.reasonKeyedVector
Section titled “KeyedVector”One decoded vector under the key the ranking reports. Keys must be unique.
Properties
Section titled “Properties”readonly key: string;readonly vec: Float32Array;MergeDecision
Section titled “MergeDecision”A committed merge: fold dropPath into keepPath.
Properties
Section titled “Properties”dropPath
Section titled “dropPath”readonly dropPath: string;keepPath
Section titled “keepPath”readonly keepPath: string;similarity
Section titled “similarity”readonly similarity: number;MergeOutcome
Section titled “MergeOutcome”One candidate pair’s fate. committed is the only stage that produces a MergeDecision.
Properties
Section titled “Properties”readonly pair: MergePair;readonly stage: MergeStage;MergePair
Section titled “MergePair”One oriented candidate pair. keepPath is already the canonical (the older file). The
orientation is the caller’s, because it depends on commit dates this module does not read.
The texts are optional; when either is absent the divergence guards are skipped.
Properties
Section titled “Properties”dropPath
Section titled “dropPath”readonly dropPath: string;dropText?
Section titled “dropText?”readonly optional dropText?: MergeText;keepPath
Section titled “keepPath”readonly keepPath: string;keepText?
Section titled “keepText?”readonly optional keepText?: MergeText;similarity
Section titled “similarity”readonly similarity: number;MergeText
Section titled “MergeText”The two texts the veto reads for one memory: its one-line claim (the <mark> gist) and the whole
article’s text. The predicates do not all read the same one, and that is the point of the split.
numericTokenDivergent reads the GIST alone. A body is where a memory cites its evidence — a date, a
pull-request or ticket id, an exit code, a cosine — and two honest tellings of one fact almost never
cite the same numbers, so a numeric set-equality over the whole article vetoed nearly every true
duplicate the store proposed (349 of 349 candidates over two consecutive runs, every review task
naming this predicate). A number in the CLAIM is the fact itself: “retry 3” against “retry 13” is two
claims, and the gist is where that difference lives.
negationDivergent and variantQualifierDivergent read the JOINED text, gist and body together.
Their markers are rare, load-bearing words (“not”, “never”, “pro”, “deprecated”) that a body does not
carry incidentally the way it carries a citation, and a polarity flip stated only in the body — the
claim “run the cutover during business hours” over a body that says draining does NOT complete — is a
contradiction the veto has to see. The two all-veto runs named neither predicate once.
Properties
Section titled “Properties”readonly body: string;readonly gist: string;MmrCandidate
Section titled “MmrCandidate”A candidate for diversification. score is its fused relevance (unitless, higher is
better); vector is its embedding, or undefined when the chunk has no vector yet, which
happens for a lexical-floor hit or a deferred embed.
Properties
Section titled “Properties”readonly path: string;readonly score: number;vector?
Section titled “vector?”readonly optional vector?: readonly number[];NeighborOptions
Section titled “NeighborOptions”The selection knobs every pair consumer states.
Properties
Section titled “Properties”readonly floor: number;Pairs below this similarity never enter ranking.
readonly limit: number;Global cap, applied after the final sim DESC, src ASC, dst ASC ordering.
perSourceK
Section titled “perSourceK”readonly perSourceK: number;Nearest neighbors kept per src, ordered sim DESC then dst ASC.
NeighborPair
Section titled “NeighborPair”One selected pair. sim is cosine similarity, unitless in [-1, 1].
Properties
Section titled “Properties”readonly dst: string;readonly sim: number;readonly src: string;ReprieveInput
Section titled “ReprieveInput”What the reprieve score reads. Units and scopes:
importance: 1-based ordinal in[1, 10], clamped; divided by 10 before use.accessCount: a quantity, per-memory lifetime, 0-based.outcomeScore: unitless in[-1, 1]. A negative value contributes 0.hoursSinceAccess: fractional hours, non-negative.
Properties
Section titled “Properties”accessCount
Section titled “accessCount”readonly accessCount: number;decayRate?
Section titled “decayRate?”readonly optional decayRate?: number;hoursSinceAccess
Section titled “hoursSinceAccess”readonly hoursSinceAccess: number;importance
Section titled “importance”readonly importance: number;outcomeScore
Section titled “outcomeScore”readonly outcomeScore: number;RetentionInput
Section titled “RetentionInput”The raw per-memory inputs the SQL phase gathers. Kept raw rather than pre-normalized so the normalization curves live in exactly one place, computeSignals.
Units and scopes, per field:
memoryTypeselects the weight profile and the half-life.ageDays: fractional days since the memory’s own last update. Non-negative.accessCount: a quantity, per-memory lifetime, 0-based.confidence: unitless in[0, 1]; clamped rather than rejected.graphRank/maxGraphRank: PageRank scores in the same run’s scale;maxGraphRankis that run’s maximum, so the ratio is corpus-relative, not absolute.bridgeCount: a quantity of cross-community memory edges for this memory.reinforcementCount: a quantity of inboundsupports/reinforcesedges.wordCount: a quantity of words in the memory’s body.contradictionCount: a quantity of authored (derived: false) contradictions; a sleep-mined suspicion is excluded upstream, so a machine guess cannot evict.
Properties
Section titled “Properties”accessCount
Section titled “accessCount”readonly accessCount: number;ageDays
Section titled “ageDays”readonly ageDays: number;bridgeCount
Section titled “bridgeCount”readonly bridgeCount: number;confidence
Section titled “confidence”readonly confidence: number;contradictionCount
Section titled “contradictionCount”readonly contradictionCount: number;graphRank
Section titled “graphRank”readonly graphRank: number;maxGraphRank
Section titled “maxGraphRank”readonly maxGraphRank: number;memoryType
Section titled “memoryType”readonly memoryType: string;reinforcementCount
Section titled “reinforcementCount”readonly reinforcementCount: number;wordCount
Section titled “wordCount”readonly wordCount: number;RetentionScore
Section titled “RetentionScore”The scoring result: the composite, its band, and the normalized signals behind it.
Properties
Section titled “Properties”action
Section titled “action”readonly action: "keep" | "compress" | "evict";readonly score: number;signals
Section titled “signals”readonly signals: Signals;Type Aliases
Section titled “Type Aliases”MergeStage
Section titled “MergeStage”type MergeStage = "cap" | "threshold" | "vetoed" | "self" | "role-guard" | "committed";Where a candidate pair stopped in mergeOutcomes, in the filter’s own order.
MergeVetoReason
Section titled “MergeVetoReason”type MergeVetoReason = "negation" | "numeric" | "variant";The three divergence families, in the veto’s own disjunction order.
ReinforceSignal
Section titled “ReinforceSignal”type ReinforceSignal = typeof REINFORCE_SIGNALS[number];SignalName
Section titled “SignalName”type SignalName = typeof SIGNAL_NAMES[number];Signals
Section titled “Signals”type Signals = { readonly [K in SignalName]: number };A full set of normalized signal values, each unitless in [0, 1].
TriageAction
Section titled “TriageAction”type TriageAction = typeof TRIAGE_ACTIONS[number];WeightProfile
Section titled “WeightProfile”type WeightProfile = { readonly [K in SignalName]: number };A weight profile: one coefficient per signal, the eight summing to exactly 1.0.
Variables
Section titled “Variables”CONFIDENCE_COMMIT_DELTA
Section titled “CONFIDENCE_COMMIT_DELTA”const CONFIDENCE_COMMIT_DELTA: 0.005 = 0.005;The smallest confidence change worth committing. Confidence decay is the widest commit in a sleep run, one meta line across many files, so a sub-threshold delta is dropped rather than committed, keeping the night’s diff reviewable.
DEFAULT_CONFIDENCE_DECAY_ALPHA
Section titled “DEFAULT_CONFIDENCE_DECAY_ALPHA”const DEFAULT_CONFIDENCE_DECAY_ALPHA: 0.1 = 0.1;The per-sleep-cycle confidence decay weight. Gentler than DEFAULT_EWMA_ALPHA on purpose: confidence should erode over many unreinforced nights rather than collapse in one, so a single missed reinforcement is forgiving. At 0.1 a claim closes a tenth of its distance to the floor per cycle.
DEFAULT_CONFIDENCE_FLOOR
Section titled “DEFAULT_CONFIDENCE_FLOOR”const DEFAULT_CONFIDENCE_FLOOR: 0.2 = 0.2;The floor confidence erodes toward but never past. A claim that stops being reinforced loses weight without vanishing: an old uncorroborated claim is weak evidence, not absent evidence, and a memory decayed to 0 would be indistinguishable from a retracted one.
DEFAULT_EWMA_ALPHA
Section titled “DEFAULT_EWMA_ALPHA”const DEFAULT_EWMA_ALPHA: 0.3 = 0.3;The outcome EWMA’s weight on an incoming signal. Its production twin is
OUTCOME_EWMA_ALPHA in packages/index/src/reinforce.ts, the value the reinforce SQL
binds; both packages’ tests pin their constant to 0.3 so a fork fails loudly.
DEFAULT_HALF_LIFE_DAYS
Section titled “DEFAULT_HALF_LIFE_DAYS”const DEFAULT_HALF_LIFE_DAYS: 30 = 30;Half-life in days for a type with no listed one.
DEFAULT_WEIGHTS
Section titled “DEFAULT_WEIGHTS”const DEFAULT_WEIGHTS: WeightProfile;The profile for a type with no dedicated one. Also sums to exactly 1.0.
EVICT_THRESHOLD
Section titled “EVICT_THRESHOLD”const EVICT_THRESHOLD: 0.3 = 0.3;HALF_LIVES_DAYS
Section titled “HALF_LIVES_DAYS”const HALF_LIVES_DAYS: Readonly<Record<string, number | null>>;Recency half-lives in days. null means no time decay. An unlisted type takes
DEFAULT_HALF_LIFE_DAYS.
KEEP_THRESHOLD
Section titled “KEEP_THRESHOLD”const KEEP_THRESHOLD: 0.7 = 0.7;Band edges. KEEP is > 0.7, EVICT is <= 0.3, and COMPRESS is the open interval between.
Each boundary is owned by the lower band: exactly 0.7 compresses, exactly 0.3
evicts. The three bands partition [0, 1] with no gap and no overlap.
LABEL_PROPAGATION_MAX_SWEEPS
Section titled “LABEL_PROPAGATION_MAX_SWEEPS”const LABEL_PROPAGATION_MAX_SWEEPS: 100 = 100;Label-propagation sweep cap. Sorted-order propagation settles well inside it.
const LN2: number = Math.LN2;The recency decay constant. Math.LN2 rather than a 0.693 literal, so
exp(-LN2 * age / halfLife) is exactly 0.5 at age == halfLife. The half-life is
then the definition of the curve rather than an approximation of it, and a property test
can assert the equality instead of a tolerance.
MAX_MERGE_PAIRS
Section titled “MAX_MERGE_PAIRS”const MAX_MERGE_PAIRS: 100 = 100;Merge decisions applied per sleep cycle, so a flood cannot fan the phase out unboundedly.
MAX_REPRIEVES
Section titled “MAX_REPRIEVES”const MAX_REPRIEVES: 3 = 3;Reprieves a memory may earn before it is forced to expire. 0 is the kill switch that
restores pure-age TTL: the floor alone can never force expiry, because the reprieve score
is a sum of non-negative terms and is therefore always above 0.
MIN_COMMUNITY_SIZE
Section titled “MIN_COMMUNITY_SIZE”const MIN_COMMUNITY_SIZE: 3 = 3;Communities smaller than this are not communities; compress batching ignores them.
MMR_LAMBDA
Section titled “MMR_LAMBDA”const MMR_LAMBDA: 0.5 = 0.5;Maximal-marginal-relevance’s relevance/diversity split.
NEAR_DUPLICATE_THRESHOLD
Section titled “NEAR_DUPLICATE_THRESHOLD”const NEAR_DUPLICATE_THRESHOLD: 0.92 = 0.92;Cosine similarity above which two bodies are the same content. Strict.
NEG_ONE_FP
Section titled “NEG_ONE_FP”const NEG_ONE_FP: number = -SCALE;PAGERANK_DAMPING
Section titled “PAGERANK_DAMPING”const PAGERANK_DAMPING: 0.85 = 0.85;PageRank teleport factor. The networkx default, kept from the predecessor memory system.
PAGERANK_MAX_ITERATIONS
Section titled “PAGERANK_MAX_ITERATIONS”const PAGERANK_MAX_ITERATIONS: 100 = 100;Power-iteration cap. Convergence at this graph’s scale is well inside it.
PAGERANK_TOLERANCE
Section titled “PAGERANK_TOLERANCE”const PAGERANK_TOLERANCE: 0.000001 = 1e-6;L1 convergence tolerance per node.
POS_ONE_FP
Section titled “POS_ONE_FP”const POS_ONE_FP: 10000 = SCALE;+1.0 and -1.0 on the grid. The outcome EWMA’s domain is [NEG_ONE_FP, POS_ONE_FP].
REINFORCE_COOLDOWN_S
Section titled “REINFORCE_COOLDOWN_S”const REINFORCE_COOLDOWN_S: 900 = 900;Seconds an access bump waits before it counts again. Stated once here and once in the salience arm’s SQL; a property test pins the two to agree at the boundary.
REINFORCE_SIGNALS
Section titled “REINFORCE_SIGNALS”const REINFORCE_SIGNALS: readonly ["positive", "negative", "neutral"];The signal a reinforcement carries. negative is what drives the outcome EWMA down.
REPRIEVE_DAYS
Section titled “REPRIEVE_DAYS”const REPRIEVE_DAYS: 14 = 14;Days a reprieve extends memhtml-valid-until by.
REPRIEVE_FLOOR
Section titled “REPRIEVE_FLOOR”const REPRIEVE_FLOOR: 0.5 = 0.5;The reprieve gate’s floor. A TTL-passed memory scoring at least this, under the reprieve
cap, has its memhtml-valid-until extended instead of being archived.
REPRIEVE_W_ACCESS
Section titled “REPRIEVE_W_ACCESS”const REPRIEVE_W_ACCESS: 0.3 = 0.3;REPRIEVE_W_IMPORTANCE
Section titled “REPRIEVE_W_IMPORTANCE”const REPRIEVE_W_IMPORTANCE: 0.4 = 0.4;The four reprieve coefficients. They sum to 1.0 but the score is NOT convex.
REPRIEVE_W_OUTCOME
Section titled “REPRIEVE_W_OUTCOME”const REPRIEVE_W_OUTCOME: 0.2 = 0.2;REPRIEVE_W_RECENCY
Section titled “REPRIEVE_W_RECENCY”const REPRIEVE_W_RECENCY: 0.1 = 0.1;const RRF_K: 60 = 60;Reciprocal-rank-fusion’s rank offset. 60 is the published default and the value the retrieval SQL inlines as a literal, so it lives here once and the assembler reads it rather than restating it.
SALIENCE_DECAY_RATE
Section titled “SALIENCE_DECAY_RATE”const SALIENCE_DECAY_RATE: 0.01 = 0.01;Salience decay rate per hour for the reprieve score’s recency term.
const SCALE: 10000 = 10_000;The fixed-point scale: 10^4, so the grid step is the 4th decimal place.
SCORE_PRECISION
Section titled “SCORE_PRECISION”const SCORE_PRECISION: 4 = 4;Decimal places the composite is rounded to, matching the predecessor’s grain.
SIGNAL_NAMES
Section titled “SIGNAL_NAMES”const SIGNAL_NAMES: readonly ["recency", "accessFrequency", "confidence", "pagerank", "bridgeImportance", "reinforcementCount", "contentDensity", "contestedStatus"];The eight signals, in the fixed order every weight profile keys on.
TRIAGE_ACTIONS
Section titled “TRIAGE_ACTIONS”const TRIAGE_ACTIONS: readonly ["keep", "compress", "evict"];The triage verdict for one memory.
WEIGHT_PROFILES
Section titled “WEIGHT_PROFILES”const WEIGHT_PROFILES: Readonly<Record<string, WeightProfile>>;Per-type weight profiles. Every profile’s eight weights sum to exactly 1.0 under
compensated summation, which is what makes the composite a convex combination of eight
[0, 1] signals and therefore itself in [0, 1].
The five profiles below are the typed ones; every other memory type falls back to
DEFAULT_WEIGHTS. Recency carries the most weight for episodic (time is that
type’s identity) and zero for procedural (a working procedure does not stale).
Functions
Section titled “Functions”applyMmr()
Section titled “applyMmr()”function applyMmr( candidates, limit, lambda?): readonly MmrCandidate[];Greedily reorder candidates by lambda * relevance - (1 - lambda) * maxSimilarityToSelected.
lambda is unitless in [0, 1]: 1 is pure relevance, 0 is pure diversity. At lambda >= 1
the function short-circuits and returns the input order truncated, because the penalty term
is multiplied by zero and the greedy pass would otherwise burn O(n^2) cosines to reproduce
the order it was given.
A candidate with no vector takes penalty 0, which is how “unknown similarity” reads here. A vectorless candidate cannot be shown to duplicate anything, so it is not penalized for it. Vectorless candidates therefore keep their relative fusion order among themselves rather than being shuffled by a fabricated distance.
The output is always a duplicate-free subsequence of the input by membership: each candidate is selected at most once and nothing is invented.
Parameters
Section titled “Parameters”candidates
Section titled “candidates”readonly MmrCandidate[]
number
lambda?
Section titled “lambda?”number = MMR_LAMBDA
Returns
Section titled “Returns”readonly MmrCandidate[]
applyNegativeHitsFp()
Section titled “applyNegativeHitsFp()”function applyNegativeHitsFp( alphaFp, prevFp, hits): number;Decay a score toward -1.0 over hits negative corrections. hits <= 0 is a no-op, so
re-running a phase over an already-drained watermark writes the same value.
Parameters
Section titled “Parameters”alphaFp
Section titled “alphaFp”number
prevFp
Section titled “prevFp”number
number
Returns
Section titled “Returns”number
applyOutcomesFp()
Section titled “applyOutcomesFp()”function applyOutcomesFp( alphaFp, prevFp, signalsFp): number;Fold signals through ewmaStepFp, one rounding per step. Rounding inside the fold rather than once at the end is what makes an N-signal batch agree with N single-signal calls. Sleep may process a memory’s corrections in one batch or across several nights, and both paths must reach the same score.
Parameters
Section titled “Parameters”alphaFp
Section titled “alphaFp”number
prevFp
Section titled “prevFp”number
signalsFp
Section titled “signalsFp”readonly number[]
Returns
Section titled “Returns”number
bandFor()
Section titled “bandFor()”function bandFor(score): "keep" | "compress" | "evict";The band a composite falls in. Boundaries belong to the lower band: 0.7 compresses and
0.3 evicts, so the bands partition [0, 1] and no score is ever unbanded.
Parameters
Section titled “Parameters”number
Returns
Section titled “Returns”"keep" | "compress" | "evict"
bridgeCounts()
Section titled “bridgeCounts()”function bridgeCounts( nodes, edges, communities): ReadonlyMap<string, number>;Per-node count of incident memory edges that cross a community boundary. This is the
bridgeImportance signal’s raw input. A node in no community (below the size floor) has no
boundary to cross, so its bridge count is 0 rather than its full degree; counting those
would make every isolated pair look maximally structural.
Parameters
Section titled “Parameters”readonly string[]
readonly GraphEdge[]
communities
Section titled “communities”ReadonlyMap<string, string | undefined>
Returns
Section titled “Returns”ReadonlyMap<string, number>
compensatedSum()
Section titled “compensatedSum()”function compensatedSum(values): number;Neumaier compensated summation. Naive left-to-right addition of the retention weight
profiles gives 0.9999999999999999 for two of the six (verified in node), which would
make a convexity assertion fail against a profile that is correct by construction. This
is the math.fsum the ported scorer relies on.
Parameters
Section titled “Parameters”values
Section titled “values”Iterable<number>
Returns
Section titled “Returns”number
compositeScore()
Section titled “compositeScore()”function compositeScore(signals, profile): number;The weighted composite, unitless in [0, 1], rounded to SCORE_PRECISION places.
Compensated summation, so the fold does not accumulate the drift that makes a
by-construction-convex profile score marginally above 1.
Parameters
Section titled “Parameters”signals
Section titled “signals”profile
Section titled “profile”Returns
Section titled “Returns”number
computeSignals()
Section titled “computeSignals()”function computeSignals(input): Signals;Normalize the raw inputs to their [0, 1] signal values.
Parameters
Section titled “Parameters”Returns
Section titled “Returns”connectedComponents()
Section titled “connectedComponents()”function connectedComponents(edges): readonly readonly string[][];Connected components over an undirected edge list, as sorted member lists.
The near-duplicate graph’s components are dedup’s units of work. A component is what “these memories might all be one memory” looks like before anything has judged them, and it is the right unit because near-duplication is transitive in practice: three rewordings of one fact produce three edges, and folding them one pair at a time would ask the same question three times and could answer it three different ways.
The partition is order-INVARIANT, not merely order-stable. A union always keeps the lexicographically smaller root, so every set’s root is the smallest key it holds no matter which order the edges arrive in. Members come back sorted, and components come back ordered by root, which is each component’s own smallest member. So the same edge SET produces the same output whether it arrives mined-first, frame-first, mirrored, or shuffled. That is stronger than sorting the input would be, and it is why no sort happens here: a caller cannot make this disagree with itself by changing how it enumerates.
A “larger root wins” or “first root seen wins” rule would break exactly that, because both make the surviving root a fact about arrival order rather than about the set.
Each pair is normalized before it is unioned, so (a, b) and (b, a) are one edge. A self-edge
introduces its key and joins nothing. Cost is near-linear in the edge count, and no step here ever
enumerates a pair the caller did not hand over.
Parameters
Section titled “Parameters”readonly readonly [string, string][]
Returns
Section titled “Returns”readonly readonly string[][]
cosine()
Section titled “cosine()”function cosine(a, b): number;Cosine similarity of two vectors, unitless and clamped to [-1, 1].
Total: every input maps to a number in range, and NaN is never returned. A zero-magnitude
input yields 0, and so does a vector carrying a NaN or infinite component — the two
conditions the arithmetic below cannot answer. 0 is the reading a vectorless candidate
already gets in MMR: no usable direction, so no demonstrable duplication, so no penalty.
Returning NaN would be worse than returning a wrong number. MMR folds a max over the
similarities to the already-selected set, and the two equivalent ways of writing that fold
disagree on NaN — Math.max(penalty, NaN) is NaN while NaN > penalty is false — so a
single degenerate embedding would make the result depend on which fold runs, not on the
corpus.
The result is clamped because the unclamped ratio does not stay in range. Verified in node
2026-08-02: two vectors whose squared magnitudes fall into the subnormal range return
1.000000106821595. The squares underflow, so sqrt divides by a magnitude smaller than
the true one. Similarity is also the input to 1 - similarity distance and to the MMR
penalty, both of which state a range, so the clamp belongs here rather than at each reader.
Length mismatch is handled by walking the shorter vector rather than failing. The only way
two stored vectors differ in length is a half-migrated embedding model, a condition the
index refuses at the embed_model watermark, so it does not reach here.
ArrayLike rather than ReadonlyArray so a Float32Array decoded straight off a stored
blob is an argument: this is the vector_distance_cos SQL function’s own body, called once
per candidate row, and materialising two 1024-element arrays per call to satisfy a narrower
type would allocate more than the arithmetic costs. Every array caller still fits.
Parameters
Section titled “Parameters”ArrayLike<number>
ArrayLike<number>
Returns
Section titled “Returns”number
cosineDistance()
Section titled “cosineDistance()”function cosineDistance(a, b): number;Cosine distance, 1 - similarity, unitless in [0, 2]. This is the space the vector
arm’s SQL works in (vector_distance_cos), so a threshold stated as a similarity is
converted once here rather than inverted at each call site.
Parameters
Section titled “Parameters”ArrayLike<number>
ArrayLike<number>
Returns
Section titled “Returns”number
decayConfidence()
Section titled “decayConfidence()”function decayConfidence( confidence, alpha?, floor?): number;One confidence-decay step in the [0, 1] float space the HTML memhtml-confidence meta uses.
alpha is the fraction of the remaining distance to floor closed per cycle, both
unitless in [0, 1].
Parameters
Section titled “Parameters”confidence
Section titled “confidence”number
alpha?
Section titled “alpha?”number = DEFAULT_CONFIDENCE_DECAY_ALPHA
floor?
Section titled “floor?”number = DEFAULT_CONFIDENCE_FLOOR
Returns
Section titled “Returns”number
decayConfidenceFp()
Section titled “decayConfidenceFp()”function decayConfidenceFp( alphaFp, confFp, floorFp): number;One confidence-decay step on the grid: an EWMA toward the floor, then a min with the
previous value.
The min is what makes the step unconditionally non-increasing. Without it, a claim
already below the floor (one an operator correction pushed down, say) would be pulled
back up toward the floor by the same convex combination that erodes a healthy claim, so
decay would rehabilitate a discredited memory. The floor is a resting place for a claim
that stops being reinforced. A refuted claim is not pulled back up to it.
Parameters
Section titled “Parameters”alphaFp
Section titled “alphaFp”number
confFp
Section titled “confFp”number
floorFp
Section titled “floorFp”number
Returns
Section titled “Returns”number
decayConfidenceN()
Section titled “decayConfidenceN()”function decayConfidenceN( confidence, cycles, alpha?, floor?): number;decayConfidence folded over cycles unreinforced sleep cycles.
Parameters
Section titled “Parameters”confidence
Section titled “confidence”number
cycles
Section titled “cycles”number
alpha?
Section titled “alpha?”number = DEFAULT_CONFIDENCE_DECAY_ALPHA
floor?
Section titled “floor?”number = DEFAULT_CONFIDENCE_FLOOR
Returns
Section titled “Returns”number
decayConfidenceNFp()
Section titled “decayConfidenceNFp()”function decayConfidenceNFp( alphaFp, confFp, floorFp, cycles): number;Fold cycles decay steps. cycles <= 0 is a no-op, so a phase re-run within one night
writes the same value.
Parameters
Section titled “Parameters”alphaFp
Section titled “alphaFp”number
confFp
Section titled “confFp”number
floorFp
Section titled “floorFp”number
cycles
Section titled “cycles”number
Returns
Section titled “Returns”number
ewmaStepFp()
Section titled “ewmaStepFp()”function ewmaStepFp( alphaFp, prevFp, signalFp): number;One EWMA step on the grid: alpha * signal + (1 - alpha) * prev, divided back down with
floor division. Floor rather than round-half-away because it is the one rounding mode
whose N-fold composition is reproducible without a rounding-mode argument, and the
residual drift against an unrounded fold is bounded by one grid step.
For alphaFp in [0, SCALE] and both values in [NEG_ONE_FP, POS_ONE_FP] the result
stays in that domain and lies between prevFp and signalFp, so a negative signal can
never raise the score.
Parameters
Section titled “Parameters”alphaFp
Section titled “alphaFp”number
prevFp
Section titled “prevFp”number
signalFp
Section titled “signalFp”number
Returns
Section titled “Returns”number
excludeSelfSupersede()
Section titled “excludeSelfSupersede()”function excludeSelfSupersede(canonicalPath, memberPaths): readonly string[];The compress-path exclusion: the members of a batch to supersede and archive, with the canonical removed and order preserved. When a batch folds into a pre-existing canonical, a member can be that canonical, and archiving it would destroy the file just folded into.
Parameters
Section titled “Parameters”canonicalPath
Section titled “canonicalPath”string
memberPaths
Section titled “memberPaths”readonly string[]
Returns
Section titled “Returns”readonly string[]
float32View()
Section titled “float32View()”function float32View(bytes): Float32Array<ArrayBufferLike> | undefined;A Float32Array over stored bytes, copying only when it must.
Float32Array requires a 4-byte-aligned byteOffset, and a driver row’s Uint8Array may be
a view into a pooled buffer at any offset. Viewing in place is the common case and costs
nothing. A misaligned or ragged blob is copied rather than rejected, because the vector arm’s
job is to rank and a throw here would fail a whole search over one row.
Parameters
Section titled “Parameters”Uint8Array
Returns
Section titled “Returns”Float32Array<ArrayBufferLike> | undefined
frameKeyOf()
Section titled “frameKeyOf()”function frameKeyOf(gist): string | null;The frame key for one claim, or null when the claim states no frame+value shape this rule
trusts. Pure and synchronous, with no clock, no randomness, no model, and no I/O, so a full
index rebuild reproduces byte-identical keys by construction.
Case- and whitespace-insensitive, because a restated fact varies in both: "The Capital of India is X" and "the capital of India is Y" collide.
Parameters
Section titled “Parameters”string
The claim text. In memhtml, this is a memory’s <mark> claim.
Returns
Section titled “Returns”string | null
The lowercased frame, or null for no-frame-shape (stored as SQL NULL).
fromFp()
Section titled “fromFp()”function fromFp(valueFp): number;A grid value back to a float. Exact.
Parameters
Section titled “Parameters”valueFp
Section titled “valueFp”number
Returns
Section titled “Returns”number
fuseArms()
Section titled “fuseArms()”function fuseArms( armResults, weights, k?): ReadonlyMap<string, number>;Fuse per-arm ranked lists into one path -> score map by summing contributions.
armResults[i] pairs with weights[i]; an arm with no weight scores 0 throughout. Addition
commutes, so the result is order-insensitive across arms and the arm registry’s order
is a presentation detail rather than a ranking input. A duplicate path inside one arm’s
list accumulates, matching the SQL’s SUM over a UNION ALL.
Parameters
Section titled “Parameters”armResults
Section titled “armResults”readonly readonly ArmHit[][]
weights
Section titled “weights”readonly number[]
number = RRF_K
Returns
Section titled “Returns”ReadonlyMap<string, number>
fuseLateral()
Section titled “fuseLateral()”function fuseLateral( fused, order, weight, k?): ReadonlyMap<string, number>;Fold a graph-walk result into existing fused scores, treating the walk’s best-first order as
an arm’s ranked list. A node absent from fused enters at its lateral-only contribution,
which is how a memory the lexical and vector arms both missed can still surface.
An empty order or a non-positive weight returns the scores unchanged, so the lateral arm
is inert when dark and cannot fail on a cold graph.
Parameters
Section titled “Parameters”ReadonlyMap<string, number>
readonly string[]
weight
Section titled “weight”number
number = RRF_K
Returns
Section titled “Returns”ReadonlyMap<string, number>
halfLifeFor()
Section titled “halfLifeFor()”function halfLifeFor(memoryType): number | null;The recency half-life in days for a memory type; null means no time decay.
Parameters
Section titled “Parameters”memoryType
Section titled “memoryType”string
Returns
Section titled “Returns”number | null
isCommittableConfidenceChange()
Section titled “isCommittableConfidenceChange()”function isCommittableConfidenceChange(before, after): boolean;True when a decayed confidence differs enough from the stored one to be worth a commit.
Parameters
Section titled “Parameters”before
Section titled “before”number
number
Returns
Section titled “Returns”boolean
joinMergeText()
Section titled “joinMergeText()”function joinMergeText(text): string;The joined text the negation and variant predicates read: the claim, a newline, the article.
Parameters
Section titled “Parameters”Returns
Section titled “Returns”string
labelPropagation()
Section titled “labelPropagation()”function labelPropagation( nodes, edges, options?): ReadonlyMap<string, string | undefined>;Communities by label propagation over the edge list read as undirected.
Deterministic in place of a seed: nodes start labelled by themselves, sweeps visit them in sorted order, and a node adopts the highest-weighted neighboring label with the lexicographically smallest label breaking a tie. The returned labels are canonicalized to the smallest member path of each community, so the partition itself is reproducible and not only the grouping.
Communities below minCommunitySize collapse to undefined: they only feed compress
batching and the bridge count, and a pair passed off as a community would make every
cross-pair edge look like a bridge.
Parameters
Section titled “Parameters”readonly string[]
readonly GraphEdge[]
options?
Section titled “options?”maxSweeps?
Section titled “maxSweeps?”number
minCommunitySize?
Section titled “minCommunitySize?”number
Returns
Section titled “Returns”ReadonlyMap<string, string | undefined>
mergeCandidates()
Section titled “mergeCandidates()”function mergeCandidates(pairs, options?): readonly MergeDecision[];Filter oriented candidate pairs into an in-batch-consistent decision list: the committed outcomes
of mergeOutcomes, in input order, as decisions.
Parameters
Section titled “Parameters”readonly MergePair[]
options?
Section titled “options?”maxPairs?
Section titled “maxPairs?”number
threshold?
Section titled “threshold?”number
Returns
Section titled “Returns”readonly MergeDecision[]
mergeOutcomes()
Section titled “mergeOutcomes()”function mergeOutcomes(pairs, options?): readonly MergeOutcome[];Classify every oriented candidate pair, applying in order: the per-cycle cap, the strict similarity
threshold, the divergence veto (only when both texts are present), a self-merge check, and the
in-batch role guard. mergeCandidates is the committed subset; this form exists so a caller
can COUNT the refusals by cause. A phase that reported candidates - decisions as “vetoed” told an
operator that a predicate fired on pairs no predicate had seen, which is how two all-refusing runs
read as a divergence problem when half of them were role-guard skips.
The in-batch role guard needs the most explanation of the five. Every committed decision fixes its drop path as DROPPED and its keep path as KEPT for the rest of the batch, and three later uses are refused, each ruling out a distinct corruption on a transitive chain:
- A path already dropped cannot be dropped again (two keepers would each believe they absorbed it) nor become a keeper (content folded into a file this same batch archives).
- A path already a keeper cannot later be dropped. Given
(gf, a)then(b, gf), both would commit,gfabsorbsaand is then archived intob, soa’s content is superseded into a file that no longer exists. That is the loss the guard exists to prevent. Verified against the input[(gf → a), (b → gf)].
A path already a keeper MAY keep again. (k, a) then (k, b) is one page absorbing two
duplicates in one batch, and nothing on that chain is archived twice or archived after being folded
into: k survives with two supersedes links and both drops point at it. The earlier rule claimed
BOTH roles of a committed pair, which made a model group of N members fold at most ONE of them per
run — every pair a group implies names the same keeper — and reported the other N-2 as vetoes. The
guard’s job is the two corruptions above, not a fold budget per keeper; the per-cycle cap is that.
Fixing roles rather than memberships keeps the output a function of input order alone. The first decision claiming a path in a role wins, matching the SQL result-set iteration upstream, and a later pair contradicting it is skipped rather than reordering anything.
Parameters
Section titled “Parameters”readonly MergePair[]
options?
Section titled “options?”maxPairs?
Section titled “maxPairs?”number
threshold?
Section titled “threshold?”number
Returns
Section titled “Returns”readonly MergeOutcome[]
mergeVetoed()
Section titled “mergeVetoed()”function mergeVetoed(textA, textB): boolean;The veto: the disjunction of the three divergence predicates, each over its own text. Symmetric, pure, total. A vetoed pair is never a duplicate no matter its cosine.
Parameters
Section titled “Parameters”Returns
Section titled “Returns”boolean
mergeVetoReasons()
Section titled “mergeVetoReasons()”function mergeVetoReasons(textA, textB): readonly MergeVetoReason[];Which divergence predicates fire on a pair, each over the text it reads (see MergeText): negation and variant over the joined text, numeric over the gist alone. Symmetric, pure, total. Empty exactly when mergeVetoed is false. Exported so a caller that must NAME the refusal (the review task a vetoed pair becomes, the acceptance arm’s per-pair stage) reads the same scoping the filter applied, instead of re-deriving which text each predicate saw.
Parameters
Section titled “Parameters”Returns
Section titled “Returns”readonly MergeVetoReason[]
negationDivergent()
Section titled “negationDivergent()”function negationDivergent(textA, textB): boolean;True when exactly one side carries a negation marker: “X is safe” against “X is NOT safe”. Symmetric. Both sides negating, or neither, is not divergent.
Parameters
Section titled “Parameters”string
string
Returns
Section titled “Returns”boolean
numericTokenDivergent()
Section titled “numericTokenDivergent()”function numericTokenDivergent(textA, textB): boolean;True when the two texts carry different numeric tokens: “retry 3 times” against “retry 13 times”. Two texts with no numbers at all, or with identical numbers, do not trip it. Symmetric. mergeVetoReasons hands it the GIST of each memory, not the article — see MergeText for why.
Parameters
Section titled “Parameters”string
string
Returns
Section titled “Returns”boolean
pagerank()
Section titled “pagerank()”function pagerank( nodes, edges, options?): ReadonlyMap<string, number>;PageRank by power iteration, weighted, with a uniform or personalized teleport prior.
seeds present makes this personalized PageRank: the teleport distribution is the seed
weights rather than uniform, so the scores describe reachability from the query’s own
hits. That is what keeps the lateral retrieval arm query-conditioned. A uniform prior
would inject the same hub memories into every query’s results regardless of
what was asked. Seeds naming absent nodes are dropped, and an all-absent seed set falls
back to the uniform prior, which the caller reads as “no lateral result”.
Dangling nodes (no outbound edges) have their mass redistributed over the teleport distribution, so the scores sum to 1 rather than leaking.
Parameters
Section titled “Parameters”readonly string[]
readonly GraphEdge[]
options?
Section titled “options?”damping?
Section titled “damping?”number
maxIterations?
Section titled “maxIterations?”number
seeds?
Section titled “seeds?”ReadonlyMap<string, number>
tolerance?
Section titled “tolerance?”number
Returns
Section titled “Returns”ReadonlyMap<string, number>
partitionByCooldown()
Section titled “partitionByCooldown()”function partitionByCooldown( entries, now, cooldownSeconds?): object;Split paths into those whose access stamp may be bumped now and those still cooling down.
One pass, order-preserving, so memory_reinforce can answer both lists from one call.
Parameters
Section titled “Parameters”entries
Section titled “entries”readonly object[]
Date
cooldownSeconds?
Section titled “cooldownSeconds?”number = REINFORCE_COOLDOWN_S
Returns
Section titled “Returns”object
bumped
Section titled “bumped”readonly bumped: readonly string[];cooledDown
Section titled “cooledDown”readonly cooledDown: readonly string[];profileWeightSum()
Section titled “profileWeightSum()”function profileWeightSum(profile): number;A profile’s weight sum under compensated summation. Every shipped profile returns exactly
1; this is the convexity fact the composite’s [0, 1] range rests on.
Parameters
Section titled “Parameters”profile
Section titled “profile”Returns
Section titled “Returns”number
rankCandidatePairs()
Section titled “rankCandidatePairs()”function rankCandidatePairs( pairs, vectors, options): readonly NeighborPair[];Rank an ENUMERATED pair set: similarity, floor, per-source top-k, final ordering, cap.
This is the shape for a consumer whose candidate pairs come from a selective predicate — the
conflict scan’s shared-entity join — rather than from the whole pair space. The predicate runs
BEFORE ranking, exactly as a WHERE inside the ranking CTE would, so per-source top-k is
computed over passing pairs only. A pair naming a key with no vector contributes nothing, the
same outcome the SQL join’s missing-embedding row produces.
Parameters
Section titled “Parameters”readonly CandidatePair[]
vectors
Section titled “vectors”readonly KeyedVector[]
options
Section titled “options”Returns
Section titled “Returns”readonly NeighborPair[]
rankFused()
Section titled “rankFused()”function rankFused(fused): readonly object[];Fused scores as a ranked list, best first. Ties break on path ascending, so the ordering is total and reproducible. Two runs over the same corpus produce the same list, which is what the discrimination gate compares against.
Parameters
Section titled “Parameters”ReadonlyMap<string, number>
Returns
Section titled “Returns”readonly object[]
reprieveScore()
Section titled “reprieveScore()”function reprieveScore(input): number;The four-term reprieve score. Deliberately not convex: the log1p(accessCount) term
is unbounded, so the score can exceed 1. It is proven only monotone and sign-clamped.
A negative outcomeScore contributes exactly 0 and does not subtract, mirroring the
salience arm’s max(coalesce(outcome_score, 0.0), 0.0). Without that clamp a memory that
once produced a bad outcome would be punished twice, once by the outcome EWMA that already
lowered its salience, and again here by having its reprieve pushed below the floor.
Parameters
Section titled “Parameters”Returns
Section titled “Returns”number
rrfContribution()
Section titled “rrfContribution()”function rrfContribution(rank, weight): Option<number>;One arm’s contribution to a fused score. rank is 1-based within that arm’s
candidate list; weight is the arm’s configured multiplier.
A zero-weight arm and an out-of-range rank both yield None rather than 0, so a
disabled arm is structurally absent from the fold instead of silently adding a
neutral term that later arithmetic could mistake for a real score.
Parameters
Section titled “Parameters”number
weight
Section titled “weight”number
Returns
Section titled “Returns”Option<number>
rrfScore()
Section titled “rrfScore()”function rrfScore( rank, weight, k?): number;One arm’s contribution: weight / (rank + k), unitless.
Strictly decreasing in rank and linear in weight, so a weight-0 arm contributes exactly
0 and is inert in the fold. k damps the difference between the top ranks, which is what
stops one arm’s confident first place from dominating four other arms’ consensus.
Parameters
Section titled “Parameters”number
weight
Section titled “weight”number
number = RRF_K
Returns
Section titled “Returns”number
scoreRetention()
Section titled “scoreRetention()”function scoreRetention(input): RetentionScore;Score one memory: normalize, weight by its type’s profile, band the composite.
Parameters
Section titled “Parameters”Returns
Section titled “Returns”shouldBumpAccess()
Section titled “shouldBumpAccess()”function shouldBumpAccess( lastAccessedAt, now, cooldownSeconds?): boolean;The reinforcement cooldown predicate. Its twin is the salience arm’s SQL guard:
WHERE last_accessed_at IS NULL OR unixepoch('now') - unixepoch(last_accessed_at) >= 900SQL cannot call this function, so the shared source of truth is the window constant
REINFORCE_COOLDOWN_S and the boundary behavior is pinned by a property test on both
sides. The >= here matches the SQL’s >=: a stamp exactly cooldownSeconds old is
bumpable.
The cooldown exists because access_count feeds the salience RRF arm. Without it, replaying
one query ten times would inflate that memory’s salience tenfold and let a loop in an agent
rewrite the corpus’s ranking.
Parameters
Section titled “Parameters”lastAccessedAt
Section titled “lastAccessedAt”Date | undefined
Date
cooldownSeconds?
Section titled “cooldownSeconds?”number = REINFORCE_COOLDOWN_S
Returns
Section titled “Returns”boolean
shouldReprieve()
Section titled “shouldReprieve()”function shouldReprieve(input): boolean;The bounded reprieve gate. A TTL-passed memory is reprieved iff its score clears floor
AND it has been reprieved fewer than maxReprieves times. maxReprieves: 0 forces every
TTL-passed memory to expire regardless of score.
Parameters
Section titled “Parameters”floor?
Section titled “floor?”number
maxReprieves?
Section titled “maxReprieves?”number
reprieveCount
Section titled “reprieveCount”number
number
Returns
Section titled “Returns”boolean
signalValue()
Section titled “signalValue()”function signalValue(signal): number;The outcome-EWMA signal value for a reinforcement, unitless in [-1, 1]. neutral is 0, so
a neutral reinforcement bumps the access count without moving the outcome score. A memory
being read is evidence of relevance, not of correctness.
Parameters
Section titled “Parameters”signal
Section titled “signal”"positive" | "negative" | "neutral"
Returns
Section titled “Returns”number
toFp()
Section titled “toFp()”function toFp(value): number;A float in [-1, 1] onto the grid, rounded to the nearest grid point.
Parameters
Section titled “Parameters”number
Returns
Section titled “Returns”number
topNeighborPairs()
Section titled “topNeighborPairs()”function topNeighborPairs(vectors, options): readonly NeighborPair[];Per-source top-k nearest neighbors above a similarity floor, over every unordered pair.
Each pair’s similarity is computed ONCE and offered to BOTH endpoints’ neighborhoods, so the
output can hold (a, b) and (b, a) — each is a fact about a different source’s neighborhood,
and a consumer folding pairs must dedup the mirror itself (dedup-merge does, with its seen
set). A floor comparison a NaN similarity cannot pass keeps a vector carrying NaN bytes out of
every neighborhood rather than poisoning an ordering.
Parameters
Section titled “Parameters”vectors
Section titled “vectors”readonly KeyedVector[]
options
Section titled “options”Returns
Section titled “Returns”readonly NeighborPair[]
variantQualifierDivergent()
Section titled “variantQualifierDivergent()”function variantQualifierDivergent(textA, textB): boolean;True when the two bodies carry different variant qualifiers: “M1” against “M1 Pro”. Symmetric.
Parameters
Section titled “Parameters”string
string
Returns
Section titled “Returns”boolean
weightsFor()
Section titled “weightsFor()”function weightsFor(memoryType): WeightProfile;The weight profile for a memory type.
Parameters
Section titled “Parameters”memoryType
Section titled “memoryType”string