Skip to content

Adaptive Memory

lostcause edited this page Aug 24, 2026 · 2 revisions

Adaptive memory

@0xx0lostcause0xx0/polypack/activation is the adaptive-memory layer: durable, decayed relevance per node, spreading activation over edges, semantic "pulses," a budgeted/diversity-aware working-memory set, and feedback-driven learned scoring — built on top of the property graph.

import { PolyGraph, ActivationEngine } from '@0xx0lostcause0xx0/polypack'

const graph = new PolyGraph()
const engine = new ActivationEngine(graph)

graph.reinforceNode('article', 1.0, 'user_read')              // durable + synced
graph.reinforceNode('article', 0.3, 'user_read', 'project-x') // also reinforces a context
graph.suppressNode('outdated-note', 1.0, 'stale')              // durable inhibition
engine.bumpAttention('article', 0.2)                            // local only

engine.effective('article')              // durable + attention − inhibition
engine.effective('article', 'project-x') // context-scoped lens instead of global

const spread = engine.spread(['article'], { depth: 2, decay: 0.5 }) // neighbours warm up
const scores = await engine.pulse('vector search')                  // semantic region scoring
await engine.absorb('vector search')                                // pulse + reinforce above threshold

engine.workingMemory(5)
engine.workingMemory({ limit: 8, tokenBudget: 2000, diversityLambda: 0.5 })

Two tiers

  • DurableNodeActivation (score, importance, reinforcementCount, lastMeaningfulActivation, plus optional inhibition/lastInhibitedAt/ context) rides as an optional field on every node, so it persists through the snapshot/WAL and adapters, and replicates through sync.
  • Transient — runtime-only attention held by ActivationEngine, never serialized or synced.

Decay is a pure function of elapsed time anchored at lastMeaningfulActivation (0.5 ** (elapsed / halfLife)), so two replicas with the same stored state compute identical current scores. inhibition decays independently against lastInhibitedAt (12h default half-life, shorter than score so suppression fades unless reinforced) and is subtracted from score only at the final read/ranking layer (effective) — never inside pulse's composite or spread — so a suppressed node stays re-evaluable, not permanently invisible. Each context entry decays independently against its own anchor (same curve as score by default) and is an additional lens on top of the global score, not a replacement.

Synchronization is additive for deltas (coalesced and gated by activationSyncThreshold, default 0.05) and max for total-state node payloads — activation is accumulated knowledge, not last-write-wins data (mergeActivation).

Durable primitives (on PolyGraph)

  • reinforceNode(id, amount, reason?, context?) / reinforceNodeSafe(...) — decay-correct to now, add amount to score, fold a fraction into importance, increment reinforcementCount, re-anchor lastMeaningfulActivation. Emits activation_updated.
  • suppressNode(id, amount, reason?) / suppressNodeSafe(...) — durable suppression delta on inhibition. A negative amount releases suppression. Emits inhibition_updated.
  • getActivation(id, halfLifeMs?), getActivationState(id), getContextActivation(id, context) — reads (context reads never fall back to the global score; a node with no history in context reads cold there).
  • topActivated(limit, minScore?) — loaded nodes ranked by current activation descending (the working-memory primitive without an engine).
  • decay(now?) — materializes decayed values for all loaded nodes and re-anchors them; reads already decay lazily, so this only matters before eviction-driven lifecycle events.

Contradiction & consolidation

  • supersede(id, supersededId, amount = 1, reason = 'superseded') — records id.supersedes = supersededId, adds a SUPERSEDED_BY edge (reference, no cascade), and suppresses the superseded node so retrieval prefers the newer one without deleting the old one. Mechanism, not policy — it doesn't detect contradictions, only acts on ones the caller identifies.
  • consolidate(node, sourceIds, options?) — writes node via addNode (insert-or-replace, so passing an existing id extends a prior consolidation), merges sourceIds into derivedFrom (deduplicated, not overwritten — re-consolidating as evidence accumulates is normal), adds CONSOLIDATED_FROM edges, and suppresses each source (options.suppressAmount default 1, options.reason default 'consolidated').

ActivationEngine

new ActivationEngine(graph, config?) composes the scoring layer. config: scoreHalfLifeMs (24h), importanceHalfLifeMs (30d), importanceGain (0.05), spreadDecay (0.5), spreadDepth (2), recencyHalfLifeMs (7d), weights (all 1), minReinforceDelta (0.05), pulseThreshold (0), absorbThreshold (0.3), absorbGain (0.05), classHalfLives (per-MemoryClass overrides).

  • reinforce/reinforceAll/suppress — call through to the PolyGraph durable primitives above. inhibitionOf(id) reads current inhibition.
  • bumpAttention(id, amount) / attentionOf(id) — the transient tier. Accumulates locally and is promoted to durable reinforcement once it clears minReinforceDelta, so tiny events (scrolls, focus) stay local while meaningful ones persist and sync.
  • effective(id, context?) — durable decayed score (or the context-scoped score) plus attention, minus decayed inhibition. Decay uses the node's resolved memory-class half-life when it has one (resolveHalfLives), else the flat config default.
  • resolveHalfLives(node)node.memoryClass if set, else the owning type's registered default, else the flat config half-lives. Built-in class defaults: episodic 12h score / 7d importance, semantic 7d / 90d, procedural 7d / 60d, entity 30d / non-decaying.
  • spread(seeds, { depth?, decay?, edgeTypes? }) — spreading activation: each hop attenuates by decay; multiple paths to a node sum. Returns { nodeId: contribution }.
  • pulse(text | vector, { topK?, semanticThreshold?, pulseThreshold?, context?, ... }) — scores the activated region around a query: semantic seeds via vector similarity (zero-similarity nodes never seed) plus outward spreading, folded with recency and usage. Read-only.
  • absorb(input, options?) — runs pulse and durably reinforces every node whose composite clears absorbThreshold, by absorbGain * score. options.context also reinforces that context on every absorbed node.
  • workingMemory(limit?, minScore?) or workingMemory(options) — loaded nodes ranked by effective descending. The options form ({ limit?, minScore?, context?, contextFallback?, tokenBudget?, costOf?, diversityLambda?, similarityOf? }) is a budgeted, diversity-aware selection — a memory-flavoured maximal-marginal-relevance pass suited to LLM context assembly: greedily picks the highest relevance − diversityLambda × similarity-to-selected candidate under tokenBudget. similarityOf defaults to cosine similarity of node.vector.
  • workingMemoryPersisted(options?) — the same adaptive ranking over all persisted nodes, not just loaded ones (transient attention unavailable for cold nodes). contextFallback: true uses global activation when a node has no history in the requested context.
  • estimateNodeTokens(node) — conservative JSON-size token estimate; usable as costOf for a token budget.
  • scoreBreakdownOf(node, semantic, graphContribution) — raw and weighted semantic/graph/recency/usage components plus total.
  • recordFeedback(id, wasUseful, learningRate = 0.05) — nudges the composite weights (used by pulse) toward whichever signal was strongest for id last time it was scored, clamped non-negative. Simple exponential-moving-average-style nudge, not a full online learner. In-memory only — not persisted or synced; getWeights()/setWeights() let an application persist and restore a snapshot across sessions.
  • dispose() — unsubscribe from graph changes and drop transient attention.

React

useWorkingMemory(graph, limit?, deps?, delay?, nodeTypes?, engine?) — a live view of the current working memory, re-queried after any graph change including activation_updated. Without engine it ranks by graph.topActivated; pass an ActivationEngine to rank by engine.workingMemory instead. See the full API reference for the other hooks.

Related projects

polypack-mcp exposes this layer as MCP tools (memory_store, memory_recall, memory_feedback, memory_suppress, memory_supersede, memory_consolidate, ...) for LLM agents. Four-Agents-Polypack uses it as the sole shared state for a multi-agent collaboration experiment.


Back to Home.

Clone this wiki locally