Polypack is ESM-only and requires Node.js 18 or newer. The browser persistence
adapter additionally requires the File System Access API (OPFS). React is
optional and only loaded by @0xx0lostcause0xx0/polypack/react.
This reference documents the TypeScript package. The Python binding is a
separate package with snake_case method names and a deliberately smaller
persisted-query surface; see python/README.md for its
public API and binding-specific behavior.
new PolyGraph(
adapter?: PersistenceAdapter,
hotCacheMax?: number,
embedding?: EmbeddingProvider,
transform?: DataTransform,
createVectorIndex?: (onChange: (id: string) => void) => VectorIndexLike,
)The main property-graph container. Without an adapter it uses MemoryAdapter.
The default hot-node limit is 50,000. Edges remain indexed when nodes are
evicted, and dirty evicted nodes are retained until persistence completes.
Synchronous queries and mutations operate on currently loaded nodes; use
getNodeSafe(id) to restore an evicted node before mutating it.
The optional transform parameter provides serialize/deserialize hooks for
non-cloneable data (Blob, File, etc.) that cannot pass through
structuredClone. See the DataTransform section below.
The optional createVectorIndex factory swaps the vector engine backing the
public vectors property (a VectorIndex by default). It accepts anything
implementing VectorIndexLike — the structural interface shared by
VectorIndex, HNSWIndex (see Vector search below), and the native engines
from @0xx0lostcause0xx0/polypack-native (NativeVectorIndex/
NativeHnswIndex via createNativeVectorIndex()/createNativeHnswIndex) —
so any of them can be swapped in and still type-check:
import { PolyGraph, HNSWIndex } from '@0xx0lostcause0xx0/polypack'
const graph = new PolyGraph(undefined, undefined, undefined, undefined, (onChange) => new HNSWIndex(onChange))Lifecycle:
warm()/load()loads persisted nodes, vectors, and edges. Call it before querying an existing database.warm()is idempotent — subsequent calls are no-ops until the graph is cleared or disposed.flush()immediately persists queued mutations. Flushes are serialized.save()writes the complete currently loaded graph without clearing dirty state.dispose()flushes queued mutations, clears memory, then closes the adapter.clear()only clears in-memory state; it does not delete adapter contents.prune(maxNodes)removes the oldest loaded nodes and applies ownership rules.transaction(callback, options?)provides atomic read-your-own-writes, rollback, nested-transaction rejection, and post-commit events. Options may includeoperationId,actor,baseRevision, andmetadata; these values are retained in the durable mutation record when the adapter supports a change feed.
Nodes:
addNode(node)inserts or replaces a node. Replacement updates type/vector indexes. Node data and vectors are structured-cloned on entry.addNodes(nodes)inserts a batch of nodes. All inputs are validated before any are inserted (an invalid entry inserts nothing), change events are coalesced into one flush, and the persistence debounce is scheduled once for the whole batch. Prefer this over a loop ofaddNodefor large inserts.getNode(id)returns a detached snapshot of a loaded node synchronously.getNodeSafe(id)restores an evicted node from persistence when necessary.updateNode(id, data, vector?, activation?)shallow-merges data and optionally replaces its vector or durable activation.updateNodeSafe(id, data, vector?)restores an evicted node before updating it.patchNode(id, patch, options?)appliesset/unset/increment/compareAndSetoperations atomically against a loaded node. Paths underdata.(or without a prefix) target node data;updatedAtand the provenance fields below (memoryClass,confidence,source,observedAt,derivedFrom,supersedes,contradicts) are also directly patchable as top-level paths (not nested underdata.).removeNodeVector(id)andremoveNodeVectorSafe(id)explicitly clear a loaded or potentially evicted node's vector while retaining the node.removeNode(id)removes the node, all connected edges, and owned descendants.removeNodeSafe(id)restores and removes an evicted node, recursively restoring owned descendants as required. Callwarm()first for an existing database so ownership edges are indexed.whereType(type)returns detached snapshots of loaded nodes of one type.sizeandloadedSizeare the number of currently loaded nodes, not total persisted nodes.hasLoadedNode(id)checks membership in that working set.persistedSize()asynchronously returns the adapter's current node count.getNodesByType(type)loads all persisted nodes of a type (async convenience).getNodesByTypeOrdered(type, field, direction?)loads all persisted nodes of a type ordered by a data field (async convenience).countNodesByType(type)returns the persisted count for a node type (async).deleteNodesByType(type)removes all persisted nodes of a type (async).vectorsis the publicVectorIndex(or substituted engine) backing similarity search. Mutating it directly (rather than throughaddNode/updateNode) does not schedule persistence; callmarkVectorDirty(id)afterwards so the change is picked up on the next flush.
Schema (optional structural validation, opt-in per type — not a precondition for using a type or edge type):
registerNodeType(type, definition?)registersrequiredFields,dataTypes, a customvalidatehook, convenienceindexes, and a defaultmemoryClassfor nodes of that type (overridable per node, see below). Re-registering re-validates every currently loaded node of that type and rolls back if any fails.registerEdgeType(type, definition?)registerssourceTypes/targetTypes,cardinality,requiredFields,dataTypes, and a customvalidatehook.nodeTypes/edgeTypesreturn defensive copies of the registered definitions, keyed by type.
Activation — durable primitives (see the activation subpath for the engine):
reinforceNode(id, amount, reason?, context?)applies a durable reinforcement delta to a loaded node: the prior state is decay-corrected to now,amountis added toscore, a fraction is folded intoimportance,reinforcementCountincrements, andlastMeaningfulActivationre-anchors to now. Whencontextis given, the same delta also reinforcesactivation.context[context]— an independently-decaying additional lens on top of (not a replacement for) the global score. Persists and emits anactivation_updatedchange event (withdelta/reason/context). Returns the updated node, orundefinedwhen the node isn't loaded.reinforceNodeSafe(id, amount, reason?, context?)restores an evicted node first.suppressNode(id, amount, reason?)applies a durable suppression delta to a loaded node'sinhibition(mirrorsreinforceNodebut for the inhibition axis, which decays on its own, shorter-by-default half-life and is subtracted fromscoreonly at the final read/ranking layer — never inside relational spreading — so a suppressed node stays re-evaluable rather than permanently invisible). A negativeamountreleases suppression. Emits aninhibition_updatedchange event.suppressNodeSafe(id, amount, reason?)restores an evicted node first.getActivation(id, halfLifeMs?)returns the current decay-corrected score (0 when the node has none). Decay is a pure function of elapsed time fromlastMeaningfulActivation, so replicas with the same stored state converge.getActivationState(id)returns the decay-corrected durable record (includinginhibition/lastInhibitedAt/contextwhen present) orundefined.getContextActivation(id, context)returns the decay-corrected score within one named context, or0— unlikegetActivation, this never falls back to the global score; a node with plenty of global activation but no history incontextreads cold in that context.topActivated(limit, minScore?)returns loaded nodes ranked by current activation descending — the working-memory primitive.decay(now?)materializes decayed values for all loaded nodes and re-anchors them. Reads already decay lazily, so this only matters for persisting fresh values (e.g. before eviction-driven lifecycle events).supersede(id, supersededId, amount = 1, reason = 'superseded')— the contradiction primitive: recordsid.supersedes = supersededId, adds aSUPERSEDED_BYedge fromidtosupersededId(ownership'reference', no cascade delete), and suppresses the superseded node so retrieval prefers the newer node without deleting the old one. Mechanism, not policy — it doesn't detect contradictions, only records and acts on ones the caller identifies. Returnsundefinedif either node isn't loaded.consolidate(node, sourceIds, options?)— the consolidation primitive: writesnodeviaaddNode(insert-or-replace — pass an existing id to extend a previous consolidation), mergessourceIdsinto itsderivedFrom(deduplicated against any already-stored value, not overwritten, so re-consolidating as more evidence accumulates is normal), adds aCONSOLIDATED_FROMedge fromnodeto each source, and suppresses each source (options.suppressAmount, default 1;options.reason, default'consolidated').node's content (includingmemoryClass, never forced) and which sources belong together are entirely caller-decided. Returnsundefined(writing nothing) if any source isn't loaded, or throws ifsourceIdsis empty.
Convenience — graph traversal:
walkAncestors(id, edgeType)walks the parent chain backwards through incoming edges ofedgeType. ReturnsPolyNode[]from root to start node (inclusive). Detects cycles.walkDescendants(id, edgeType)walks the child chain forwards through outgoing edges ofedgeType. ReturnsPolyNode[]from start node to deepest child (inclusive). Detects cycles.
Full-text search:
searchNodes(text, type, threshold?, topK?)shorthand forqueryPersistedText(text, threshold, topK).then(q => q.whereNodeType(type).toArray()). ReturnsPromise<PolyNode[]>.
Edges:
addEdge(source, type, target, data?, ownership?)adds one unique directed edge.getEdges(source, type?)returns detached outgoing edge descriptors.getEdgeTargets(source, type)andgetEdgeSources(target, type)return IDs.removeEdges(source, type?, target?)removes matching outgoing edges.
Ownership is stored on the edge:
reference(default): removing the edge never removes its target.shared: keeps the target, and invokes the protectedonOrphanhook when it loses its final incoming edge.owned: removes the target when it loses its final owning source; cascading is cycle-safe.
Reactivity and batching:
changesis an RxJSSubject<GraphChangeEvent>— emitsnode_added,node_updated,node_removed,edge_added,edge_removed,activation_updated(carries optionaldelta/reason/context), andinhibition_updated(carries optionaldelta/reason).startBatch()queues notifications until the matchingendBatch().endBatch()throws if no batch is open.
query() creates a mutable GraphQuery.
Public graph reads, query results, join/collection predicate values, edge data,
and vector-index reads are detached copies. Mutate graph state through graph or
vector-index methods so persistence tracking and reactive notifications remain
correct. Data values must be compatible with the platform's structuredClone.
queryPersisted() creates a mutable PersistedGraphQuery. Its terminal methods
are asynchronous and inspect the complete backing store without loading results
into the hot working set. Results are detached node copies, and mutations must be
flushed before they become visible. The persisted-query API supports node type,
attribute and range filters, edge filters, joins, traversal, collection,
ordering, similarity, offset, and limit.
Filter methods are chainable:
where(field, value)/whereAttribute(name, value)use strict equality.whereAttributeRange(name, { above?, below? })uses exclusive boundaries.whereNodeType(...types)restricts node types.whereEdge(type, target?)requires an outgoing edge.whereEdgeSource(source)requires an incoming edge from that source.join(edgeType, direction?, predicate?)filters by connected nodes.traverse(edgeType, depth, direction?)performs breadth-first traversal and includes the seed nodes.similarTo(vector, threshold?, topK?)ranks vector-bearing nodes by cosine similarity.orderBy(field, direction?),offset(n), andlimit(n)shape results.whereActivated(above)keeps only nodes whose current (decay-corrected) activation exceedsabove.orderByActivation(direction?)orders results by current activation instead of a data field (defaultdesc).
Limits, offsets, traversal depths, and top-K values must be non-negative integers. Timestamps must be finite and non-negative; vectors and numeric range boundaries must contain finite numbers. Node IDs/types and edge endpoints/types must not be empty.
Terminal methods:
toArray(),first(),count(), andids()return matched nodes or IDs.count()respects similarity, traversal, offset, and limit.pluck(...fields)projects node data into records that also containidandtype.aggregate(field, op)supportssum,avg,min,max, andcount.groupAggregate(field, op, groupByField)aggregates by a data field.having(groups, predicate)filters aggregate rows.groupByVector(groups, field, op, threshold?)assigns nodes to their nearest centroid.uniqueKeys(field)returns distinct values across all currently loaded nodes.collect(edgeType, direction?, predicate?)returns unique directly connected nodes.
Filters run before traversal. Similarity ranking runs after orderBy, so it becomes
the final ordering before offset and limit.
Chainable filters are where, whereAttribute, whereAttributeRange,
whereNodeType, whereEdge, whereEdgeSource, join, traverse, orderBy,
similarTo, whereActivated, orderByActivation, offset, and limit. The
asynchronous terminal methods are toArray(), first(), count(), ids(),
and collect(). Activation filters/ordering are applied post-load (like
similarity), so they disable adapter-side pagination.
Adapters may implement queryNodes(query) and countNodes(query) for optimized
storage-level execution, plus getEdgesBySources(ids, type?) and
getEdgesByTargets(ids, type?) for indexed graph operations. When absent,
Polypack falls back to the original node and edge methods, preserving
compatibility with existing custom adapters. countNodes({}) returns the total
persisted node count without materialising ids, and type-only queries use a
secondary type index. getEdgesBySources/getEdgesByTargets are backed by
source/target edge indexes, so persisted traversal no longer scans every edge.
For node-only queries, offset and limit are delegated to the adapter.
BinaryStoreAdapter uses an in-memory snapshot for persisted queries. Queries
with similarity or graph post-processing retain pagination in the query layer so
that filtering, traversal, and ranking occur before the page is selected.
new VectorIndex(onChange?, distanceFn?)creates an exact in-memory index.add,addMany,remove,removeMany,clear,has,get,entries, andsizemanage vectors.add/addManyinvokeonChangefor each id;hydrate(id, vector)sets a vector without triggeringonChange, for restoring persisted state without marking it dirty again.query(vector, topK, threshold?)returns{ id, score }[], highest first.cosineSimilarity(a, b)andeuclideanSimilarity(a, b)are built in.
All compared vectors must have identical dimensions; otherwise similarity
functions throw RangeError. Zero vectors have cosine similarity 0. Added
vectors are copied, and get()/entries() return detached arrays.
HNSWIndex (new HNSWIndex(onChange?, distanceFn?, config?, rng?)) is an
approximate, update-safe alternative implementing the same VectorIndexLike
surface (add/addMany/hydrate/remove/removeMany/query/clear/
has/get/entries/size). config accepts M, Mmax0, efConstruction,
and efSearch (all optional, cosine distance by default) — each, if given,
must be a positive integer, or the constructor throws RangeError. It can be
passed directly to PolyGraph's createVectorIndex hook — see the
constructor section above. update(id, vector) also exists but is
@deprecated: it's identical to add() (which already overwrites an
existing id) and isn't part of VectorIndexLike, so prefer add().
Precision is selected when creating a binary store with vectorPrecision: 'float32' or
'float64'. Existing stores adopt their recorded precision when the setting is omitted;
an explicit mismatch is rejected. To change an existing Node filesystem store, stop all
processes using it and run the offline migration helper:
import { migrateVectorPrecision } from '@0xx0lostcause0xx0/polypack/persistence/node'
const result = await migrateVectorPrecision('./graph-store', 'float32', {
backupDir: './graph-store-before-float32',
})The Node helper verifies the converted store before replacement and leaves the original
at backupDir. Python exposes the equivalent operation on an open PolyGraph:
result = graph.migrate_vector_precision("float32", "./graph-store-before-float32")Float32-to-Float64 migration widens the rounded values; it cannot restore precision discarded by an earlier Float32 conversion. Both filesystem helpers are offline operations; OPFS and other adapters require their own migration workflow.
Direct Rust filesystem users can call polypack_core::storage::migrate_vector_precision
with the store path, target VectorPrecision, and an optional backup path. It has the
same stop-all-writers requirement and retains the source directory as the backup.
-
EmbeddingProviderdefinesembed(text)and an optionaldimensions. The method may be synchronous or asynchronous and may return a number array,Float32Array, orFloat64Array. -
FeatureHashEmbeddingis the default dependency-free provider. It produces deterministic, normalized 384-dimensional lexical vectors without downloading a model. Its dimensions can be configured in the constructor. -
Pass a custom provider as the third
PolyGraphconstructor argument:const provider = { dimensions: 768, async embed(text: string) { return model.embed(text) }, } const graph = new PolyGraph(adapter, 10_000, provider)
-
embed(text)generates and validates a detachedFloat64Array. -
addNodeWithEmbedding(node, text)adds a node using generated text features. -
updateNodeWithEmbedding(id, data, text)andupdateNodeSafeWithEmbedding(id, data, text)regenerate a node vector. -
queryText(text, threshold?, topK?)returns aPromise<GraphQuery>already configured for similarity search. -
queryPersistedText(text, threshold?, topK?)does the same across the complete persisted dataset.
The default provider captures shared words rather than learned semantic meaning. Use a model-backed provider when synonyms and deeper language relationships are required. Keep one provider and dimensionality for a vector index; similarity comparisons reject vectors with mismatched dimensions.
MemoryAdapter(maxNodes?)stores serialized records in memory. WhenmaxNodesis set, the least-recently-put node (plus its edges/vector) is evicted once the cap is reached; writing a node again (viaputNode,bulkPutNodes, orapplyChanges) bumps it back to most-recently-put.BinaryStoreAdapter({ storeDir, compactThreshold?, fileIO?, syncWrites? })persists data as a MessagePack snapshot plus an append-only write-ahead log (WAL). Nodes, edges, and vectors are committed atomically per batch; the WAL is compacted into a snapshot once it passes an adaptive threshold and onclose().compactThresholdis the minimum WAL-entry count at which compaction is scheduled (default 10,000); the effective threshold also grows with the store (max(threshold, records / 4)), so a 1M-node build no longer rewrites the snapshot quadratically. Startup replays the WAL, then persists a snapshot before deleting it so a crash between those steps loses nothing. Recovery also tolerates a truncated WAL tail from a mid-append crash. Schema definitions supplied in an atomicapplyChangescommit are included in the WAL and snapshot; the legacyschemas.jsonsidecar remains readable for older stores.syncWrites: truefsyncs WAL appends and snapshot writes (including the containing directory) for crash durability at a throughput cost.BinaryStoreAdapterlives behind platform subpaths so the core entry point stays free ofnode:built-ins:@0xx0lostcause0xx0/polypack/persistence/node—NodeFileIO(filesystem).@0xx0lostcause0xx0/polypack/persistence/opfs—OPFSFileIO(browser File System Access API).@0xx0lostcause0xx0/polypack/persistence—MemoryFileIOand theFileIOtype for tests and custom storage. WhenfileIOis omitted, a platform default is created at first use.
FileIOis the storage contract:readFile,writeFile,appendFile,deleteFile,fileExists. Implement it to plug in any backing store.PersistenceAdapteris the contract for custom storage. It contains node, edge, and vector single/bulk operations plusclearAll()andclose().- Adapters that expose
changeFeedprovide the durable logical log throughgraph.mutationLogSince(sequence),graph.mutationLogPage(sequence, limit), andgraph.latestMutationSequence(). Sequences are exclusivebigintcursors, so callers can resume replication or audit scans without rereading acknowledged records. - Adapters may implement
getSchemaDefinitions()andsetSchemaDefinitions()to persist structural node/edge schema metadata. The canonical file shape isnodeTypes[{ nodeType, ... }]andedgeTypes[{ edgeType, ... }], shared with the Rust and Python bindings. Runtime validator callbacks are intentionally not serialized; applications must register those callbacks again after opening a store. PersistenceChangesdescribes one logical node/edge/vector commit. Adapters may implementapplyChanges(changes)to commit it atomically;PolyGraphprefers this hook and restores the complete dirty batch when it rejects.
Adapter methods should reject on storage errors. Bulk methods should be atomic
where the backing store permits it. MemoryAdapter applies changes through
copy-on-commit maps, while BinaryStoreAdapter appends one WAL batch covering
all three record kinds. Existing custom adapters without applyChanges remain
compatible but cannot guarantee cross-record atomicity through the fallback
path.
The root exports PolyNode, PolyEdge, EdgeOwnership, GraphChangeEvent,
SerializedNode, SerializedEdge, VectorQuery, EdgeTypes, DataTransform,
NodeActivation, ContextActivation, MemoryClass, aggregate types,
DistanceFunction, PersistenceAdapter, ActivationEngine,
WorkingMemoryOptions, ClassHalfLives, mergeActivation,
SUPERSEDED_BY_EDGE, and CONSOLIDATED_FROM_EDGE. Persistence types and
adapters beyond MemoryAdapter live under the persistence subpaths.
PolyNode also carries optional memory-class and confidence/provenance
fields, none of which affect existing nodes that don't set them:
memoryClass (MemoryClass, overrides NodeTypeDefinition.memoryClass for
this node only), confidence (finite number in [0, 1]), source (non-empty
string), observedAt (epoch ms, may predate insertedAt), derivedFrom
(node ids this node was derived/consolidated from — a soft reference, not
validated for existence), supersedes (a soft-reference node id), and
contradicts (soft-reference node ids). These are ordinary node fields, not
activation state — static until explicitly revised, riding the existing
addNode/updateNode/patchNode write paths like data.
edgeId(source, type, target) produces the persistence edge key. Source IDs and
edge types must not contain ::; target IDs may contain it.
yieldToUI() yields through a zero-delay timer.
Optional hooks for handling non-cloneable data (Blob, File, etc.) that cannot
pass through structuredClone. Pass as the fourth PolyGraph constructor
argument:
const blobStore = new Map<string, Blob>()
const transform: DataTransform = {
serialize(data) {
const copy = { ...data }
if (copy.blob instanceof Blob) {
blobStore.set(copy.id, copy.blob)
copy.blob = null
}
return { data: copy }
},
deserialize(data) {
const copy = { ...data }
if ('blob' in copy && copy.blob === null) {
copy.blob = blobStore.get(copy.id) ?? null
}
return copy
},
}
const graph = new PolyGraph(adapter, 10_000, undefined, transform)serialize returns { data: cloneableData, sidecar?: unknown }. The sidecar
is stored in memory alongside the node and re-supplied to deserialize on every
read. The sidecar is not persisted across sessions.
Create a frozen edge-type constant object with full literal-type inference:
import { defineEdges } from '@0xx0lostcause0xx0/polypack'
export const EDGE = defineEdges({
KNOWS: 'KNOWS',
LIKES: 'LIKES',
})
// typeof EDGE.KNOWS === 'KNOWS' (literal string type)Build a weighted embedding string by repeating fields according to their weight. The default weight is 1; fields with higher weights are repeated more often so the feature-hash bag-of-words embedding treats them as more significant:
import { buildEmbeddingText } from '@0xx0lostcause0xx0/polypack'
buildEmbeddingText(
{ subject: 'Hello', content: 'World' },
{ subject: 3 }
)
// => "Hello Hello Hello World"useGraphQuery(graph, queryFn, deps, delay?, nodeTypes?, onError?)
useLiveQuery(graph, querier, deps?, defaultResult?)Both hooks execute immediately, subscribe to graph changes, and support sync or
async queries. useGraphQuery defaults to a 200 ms debounce. When nodeTypes
is supplied, events for other known node types are ignored. Query errors are
logged, passed to the optional onError(error) callback, and set the result to
undefined.
The dependency list controls query closure refresh in the same way as other
React hooks. Changing nodeTypes or delay updates the active subscription
without requiring dependency-list changes. In-flight queries are serialized;
stale results are discarded after dependency changes or unmounting, and one
follow-up run is retained when mutations arrive during an asynchronous query.
React 18 and 19 are supported as an optional peer dependency.
useWorkingMemory(graph, limit?, deps?, delay?, nodeTypes?, engine?) is a live
view of the current working memory: the limit most-activated loaded nodes,
re-queried after any graph change (including activation_updated). deps
controls re-runs and must include limit when it can change. Without engine
it ranks by graph.topActivated (durable score only); pass an
ActivationEngine to rank by engine.workingMemory instead (durable score
plus transient attention from bumpAttention). Sub-threshold bumpAttention
calls never emit a graph change event, so they won't trigger an automatic
re-run on their own — include something that changes after a bump in deps
if the view needs to reflect it immediately.
The adaptive-memory layer. It splits activation into two tiers:
- Durable —
NodeActivation(score,importance,reinforcementCount,lastMeaningfulActivation, plus the optionalinhibition/lastInhibitedAt/contextdescribed below) 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.
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) // current "mental state"
engine.workingMemory({ limit: 8, tokenBudget: 2000, diversityLambda: 0.5 }) // budgeted + diversenew ActivationEngine(graph, config?)composes the scoring layer.configoptions:scoreHalfLifeMs(24 h default),importanceHalfLifeMs(30 days),importanceGain(0.05),spreadDecay(0.5),spreadDepth(2),recencyHalfLifeMs(7 days),weights(all 1),minReinforceDelta(0.05),pulseThreshold(0),absorbThreshold(0.3),absorbGain(0.05), andclassHalfLives(per-MemoryClassscore/importance half-life overrides, see below).reinforce(id, amount, reason?, context?)/reinforceAll(entries)call through toPolyGraph.reinforceNode(durable).suppress(id, amount, reason?)calls through toPolyGraph.suppressNode.inhibitionOf(id)returns the current decayed inhibition (0 when none).bumpAttention(id, amount),attentionOf(id), andeffective(id, context?)manage the transient tier.bumpAttentionaccumulates locally and is promoted to durable reinforcement once it clearsminReinforceDelta, so tiny events (scrolls, focus) stay local while meaningful ones become persisted and synced.effectiveis durable decayed score (or the context-scoped score whencontextis given) plus attention, minus decayed inhibition — decay uses the node's resolved memory-class half-life (seeresolveHalfLives) when it has one, else the flat config default.resolveHalfLives(node)resolves a node's effective score/importance half-lives:node.memoryClassif set, else the owning type's registeredNodeTypeDefinition.memoryClassdefault, else no class — the flat config half-lives, unaffected byclassHalfLives. Defaults per class: episodic 12h score / 7d importance, semantic 7d / 90d, procedural 7d / 60d, entity 30d / non-decaying.spread(seeds, { depth?, decay?, edgeTypes? })implements spreading activation: each hop attenuates the contribution bydecay; 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 (nodes with zero similarity never seed the region) plus outward spreading, folded with recency and usage. Read-only.absorb(input, options?)runspulseand durably reinforces every node whose composite clearsabsorbThresholdbyabsorbGain * score. Whenoptions.contextis given, the same reinforcement also reinforces that context on every absorbed node.workingMemory(limit?, minScore?)orworkingMemory(options)returns loaded nodes ranked byeffectiveactivation descending. The options form enables a budgeted, diversity-aware selection — a memory-flavoured maximal-marginal-relevance pass suited to LLM context assembly:{ limit?, minScore?, context?, contextFallback?, tokenBudget?, costOf?, diversityLambda?, similarityOf? }. Greedily picks the highestrelevance − diversityLambda × similarity-to-already-selectedcandidate undertokenBudget(via caller-suppliedcostOf, defaulting toestimateNodeTokens).similarityOfdefaults to cosine similarity ofnode.vector(0 if either lacks one).workingMemoryPersisted(options?)performs the same adaptive ranking over all persisted nodes without requiring them to be loaded into the hot cache. Transient attention is unavailable for cold nodes.contextFallback: trueuses global activation when a node has no history in the requested context; the default remains strict context-lens ranking.estimateNodeTokens(node)provides a conservative JSON-size token estimate for integrations without a model-specific tokenizer. Pass it ascostOfwhen using a token budget.scoreBreakdownOf(node, semantic, graphContribution)returns raw and weighted semantic, graph, recency, and usage components plus their total.recordFeedback(id, wasUseful, learningRate = 0.05)— learned weights. Nudges the compositeweights(used bypulse) toward whichever signal (semantic/graph/recency/usage) was strongest foridthe last time it was scored bypulse: each weight moves bylearningRate * direction * signal, clamped to stay non-negative. A simple exponential-moving-average-style nudge, not a full online learner. A no-op ifidhas no cached signal breakdown. Weights are in-memory only — not persisted or synced.getWeights()andsetWeights(weights)export and restore learned weights; applications that want cross-session learning should persist this snapshot and restore it when constructing an engine.dispose()unsubscribes from graph changes and drops transient attention.mergeActivation(existing, incoming, now?)merges two durable total-state records: decay-corrects both tonow, keeps the stronger component of each (including a per-key max-merge overcontextand a max-merge ofinhibition), and re-anchors tonow. Used by the sync layer (max-merge, idempotent for re-delivered snapshots).
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 — 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 (the activationUpdate/
inhibitionUpdate ops, coalesced and gated by activationSyncThreshold,
default 0.05) and max for total-state node payloads — activation is
accumulated knowledge, not last-write-wins data.
OpLog(clientId, existing?)appends sequencedSyncOpvalues and exposessince(seq),all,latestSeq, andsize.MemorySyncClientStateStoreandFileSyncClientStateStorepersist pending operations plus the acknowledged client and server cursors. Useawait SyncClient.restore({ graph, transport, stateStore, clientId? })to resume a client after restart.await client.persist()waits for the latest local state to be durable; the file store includes a checksum and rejects corrupted state.SyncAdapter(inner, clientId)wraps persistence and records successful node and edge writes in itsoplog; setonOpto observe them.SyncClient({ graph, transport, clientId?, autoFlush?, retryMs?, activationSyncThreshold?, stateStore? })captures graph events, retains operations until acknowledged, retries unacknowledged deltas, detects server-cursor gaps, and applies remote operations with echo suppression.retryMsdefaults to 1,000 ms and0disables automatic retry.activationSyncThreshold(default 0.05) drops coalesced activation deltas below that magnitude instead of syncing them. Useflush()for manual sends, inspectpendingOps, callrequestSync(fromStart?)for catch-up, inspectsyncCursor, callreconnect(transport)to replace a transport, resend pending work, and request missing server operations, and usedisconnect()to flush any pending operations and close.SyncServeris an in-memory relay by default. Pass aSyncOperationLogasoperationLogand awaitready()before accepting clients to restore and durably append the server history.maxBatchOpsbounds an incoming envelope andmaxPendingOpsapplies back-pressure to the durable queue. A rejected envelope receives anackwithpending_too_large; retry it after the queue drains. A storage failure returns apersistence_erroracknowledgement and causesawait server.flush()to reject with the underlying error, so callers can retry after repairing the log.await server.flush()waits for all durable submissions accepted so far to reach the operation log.addClient(handle)returns that client's incoming-message handler,removeClient(handle)unregisters it, andopsexposes the received log. The server deduplicates operations by client and sequence, operation, and transaction identity, acknowledges both first-time and repeated delivery, and serves full operation snapshots or cursor-based deltas for late and reconnecting clients. Authorization and conflict hooks validate transaction groups atomically: if one operation is rejected, none of that transaction is committed or broadcast.await server.logStats()reports cursor retention, retained operations, and operation/transaction identity counts for monitoring and compaction tooling.SyncTransportrequiressend,onMessage, andclose.MemoryTransport.pair()creates linked asynchronous in-process transports.
The bundled sync layer is intentionally transport-agnostic. It provides optional
durable server and client logs, authentication and conflict hooks, bounded
batches, checksums, cursor recovery, and filtered subscriptions; it does not
provide identity management, application permissions, or a domain-specific
conflict resolver. Applications must detect transport failure and supply a
replacement transport to reconnect().