weavatrix-memory
Bitemporal, evidence-carrying agent memory with a hard token budget — written
in Rust, exposed to Node.js and Bun through Node-API.
It answers two different questions that most memory systems conflate:
what was true at time T, and what did the agent know at time T. Every
fact carries both axes plus the evidence behind it, so a wrong answer can be
traced to the fact that caused it.
No LLM, no vector database, no network service, no async runtime, no external
graph database.
npm install weavatrix-memory
# or
bun add weavatrix-memory
const { Memory } = require('weavatrix-memory')
const memory = new Memory({
knownAt: 20,
nodes: [
{ id: 'task:1', kind: 'task', label: 'Fix query' },
{ id: 'file:1', kind: 'file', label: 'query.js' },
],
facts: [{
id: 'fact:1',
source: 'task:1',
relation: 'depends_on',
target: 'file:1',
validFrom: 10,
recordedAt: 12,
agentId: 'agent:1',
sessionId: 'session:1',
evidence: [{ kind: 'test', source: 'query-suite' }],
}],
})
const context = memory.compileContext({
seeds: ['task:1'],
validAt: 20,
knownAt: 20,
tokenBudget: 2_000,
maxDepth: 2,
})
context.view
context.graph
context.receipt
The two time axes
| Valid time | validFrom, validUntil | When was this true in the world? |
| Known time | recordedAt | When did the system learn it? |
A query supplies both (validAt, knownAt). Asking "what did we believe last
Tuesday about the state of the repository last Monday" is a single query, not
a reconstruction.
All timestamps are integer Unix microseconds and must stay inside
JavaScript's safe-integer range; a non-integer or out-of-range value is
rejected, never rounded.
Input
MemoryNode
id | string | Stable identity. |
kind | string | Entity category. |
label | string | Human-readable name. |
repository, branch | string? | Scope, filterable at query time. |
attributes | Record<string, string>? | |
MemoryFact
id | string | |
source, target | string | Node ids. |
relation | string | Relation type; filterable at query time. |
validFrom | number | Start of valid time. |
validUntil | number? | Open-ended when omitted. |
observedAt | number? | |
recordedAt | number | Known time. |
agentId, sessionId | string | Who asserted it, in which session. |
confidence | number? | Basis points, 0 … 10000. |
evidence | Evidence[] | { kind, source, locator?, digest? } |
supersedes | string? | The fact this one replaces. |
MemoryInput
{ nodes, facts, knownAt, sourcePosition? }
API
new Memory(input)
Validates the whole projection up front: unknown node references, a confidence
above 10,000 basis points, and malformed timestamps all throw here rather than
at query time.
nodeCount | number | |
factCount | number | |
view(validAt, knownAt) | MemoryView | { nodes, facts } after both time filters. |
graph(validAt, knownAt) | object | The projected provenance graph for that same instant. |
compileContext(request) | ContextBundle | See below. |
ContextRequest
seeds | string[] | required | Where the walk starts. |
validAt | number | required | Valid-time instant. |
knownAt | number | required | Known-time instant. |
tokenBudget | number | required | Hard ceiling. The compiler stops selecting facts before exceeding it. |
maxDepth | number | 2 | Traversal depth from the seeds. |
relations | string[] | all | Restricts which relations may be traversed. |
repositories, branches | string[] | all | Scope filters. |
ContextBundle
{ view, graph, receipt }.
The receipt is the point. It states what the compiler did, so a context can
be audited rather than guessed at:
validAt, knownAt, sourcePosition | The instant this bundle describes. |
estimator, tokenBudget, estimatedTokens | Which estimator ran, the ceiling, and what was actually spent. |
examinedFacts, selectedFacts | How much was considered versus kept. |
omittedByBudget | Facts dropped because the budget ran out. |
excludedByScope | Facts dropped by relation, repository, or branch filters. |
A context that is missing something always says which of the two reasons
applies.
Errors
InvalidArg | Unknown field, fact referencing an undeclared node, confidence above 10,000 basis points, timestamp that is not a safe integer, empty seed list. |
GenericFailure | Node or fact count exceeding the addressable range. |
What ships
| Runtimes | Node.js 18+ (Node-API 8), Bun 1.4+ |
| Platforms | Windows x64/arm64, macOS x64/arm64, glibc Linux x64/arm64 |
| Install script | none |
| Network at install | none |
| Runtime dependencies | none |
| Platform packages | none — all six bindings are in this one tarball |
Measured, without fake equivalence
benchmark/RESULTS.md is generated from the
weavatrix-benchmarks
harness.
There is no zero-network npm package that returns this contract, so the
comparison uses graphology as a topology-only floor — and it wins:
| Depth-2 neighborhood node ids on a 10,000-node chain | graphology 20x faster | graphology 9x faster |
That row is published rather than hidden, and it is not a like-for-like result.
graphology performs the overlapping topology query and nothing else.
Weavatrix additionally filters valid time and known time, checks scope and
relations, enforces the token budget, selects evidence-carrying facts,
materializes the provenance graph and the receipt, crosses Node-API, and
serializes the bundle. A depth-2 walk on a chain also reaches only five nodes,
so the competitor's median sits at timer resolution: read it as "graphology
wins the topology-only query", not as a calibrated multiple.
The Rust repository separately keeps equal-contract comparisons against
agentic-memory, where the depth-2 context kernel at 100,000 nodes returned
the same result 26.1x faster — and where the bulk constructor lost by
1.68x. That losing row is published too.
Memory owns its repository, package, release evidence, and MIT license, and can
be used entirely on its own.
Repository: Weavatrix/weavatrix-memory ·
Rust crate: crates.io/crates/weavatrix-memory ·
License: MIT