AgentStack
MCP verified Apache-2.0 Self-run

Gaptime

mcp-davccavalcante-gaptime · by davccavalcante

GapTime: bi-temporal knowledge-graph memory for production agents. Every fact carries valid time and transaction time, so you time-travel to what was known at any instant, supersede outdated facts without losing history, and detect contradictions. Zero dependencies, TypeScript, edge-ready.

No reviews yet
0 installs
6 views
0.0% view→install

Install

$ agentstack add mcp-davccavalcante-gaptime

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Gaptime? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

GapTime NPM

[](https://www.npmjs.com/package/@takk/gaptime) [](./LICENSE) [](./SPEC.md) [](./SPEC.md) [](./SPEC.md) [](./package.json)

GapTime stores bi-temporal facts for Massive Intelligence (IM) agent workloads, and it never calls a provider API. That is the product invariant: zero credentials, zero network, the host stays in full control of its own calls. Every fact carries two independent clocks, valid time (when it was true in the world) and transaction time (when GapTime recorded it), so you can ask what was true at any instant, reconstruct what the system believed at any instant, supersede an outdated fact without losing the old one, and keep an append-only history. You assert facts you already have, then time-travel and search across them, and GapTime accounts for every version.

Why it exists: conventional agent memory assumes truth is timeless. A user says "I work at Anthropic" in February, moves to OpenAI in May, and flat memory either forgets the old fact (history lost) or keeps both (a contradiction the agent repeats back). Bi-temporal indexing resolves it: the old fact was valid February to May, the new one is valid from May, and GapTime can still reconstruct exactly what it believed at any past instant. Graphiti proved the model in Python but is database-bound; as of June 2026 we found no shipping npm package that combines a four-timestamp model, contradiction detection, hybrid retrieval, provenance, and an audit history in a zero-dependency, embeddable form, and regulated agent deployments (medical, finance, legal) increasingly need exactly this temporal traceability.

What ships: a four-timestamp bi-temporal model, contradiction detection over Allen interval relations, invalidate-never-delete history with retract that heals, deterministic prefix-free statement keys (FNV-1a 64), hybrid retrieval (in-memory BM25, n-hop graph traversal, optional pluggable vector hook), provenance and lineage on every fact, an append-only frozen audit trail, a prune retention policy, memory, file, and KV persistence, structural write-through adapters for Neo4j, Postgres and SQLite, Vercel AI SDK and OpenTelemetry GenAI and Model Context Protocol ingestion, bridges for the @takk family, and a CLI with a hardened HTTP bridge. Zero runtime dependencies. 127 tests across 16 suites on Node 22 and Node 24.


See it run

This is the literal output of the deterministic demonstration that ships in the CLI. Same flags, same bytes, every run:

$ gaptime demo
gaptime 1.0.0 demo: "where does David work?"

Two facts asserted: works_at Anthropic (from Feb), works_at OpenAI (from May).
The May assertion contradicts the Feb one (single-valued predicate), so GapTime
closes the Anthropic fact at May 1 instead of deleting it.

asOf 2026-03-15 (valid-time travel):
  f3vroh52o4m96n  David works_at Anthropic  valid[2026-02-01T00:00:00.000Z, 2026-05-01T00:00:00.000Z) tx[2026-05-01T00:00:00.000Z, now) src=conversation

asOf 2026-06-15 (valid-time travel):
  f3vrf22a4mdmfq  David works_at OpenAI  valid[2026-05-01T00:00:00.000Z, open) tx[2026-05-01T00:00:00.000Z, now) src=conversation

Contradictions:
  works_at: f3vqw7wp1lwcxw superseded by f3vrf22a4mdmfq (finishedBy)

History preserved: 3 fact versions, 2 current, 1 contradiction(s).

Two assertions about the same single-valued predicate run in sequence. The May fact contradicts the February one, so GapTime closes the Anthropic fact at May 1 and records the contradiction with its Allen relation. Valid-time travel returns Anthropic in March and OpenAI in June; nothing is deleted, and the history carries all three fact versions.


Quickstart

pnpm add @takk/gaptime
# or: npm install @takk/gaptime

Assert, time-travel, search. The whole integration is this loop:

import { createGapTime } from '@takk/gaptime';

const memory = createGapTime();

// February: David joins Anthropic.
memory.assert({
  subject: { type: 'person', name: 'David' },
  predicate: 'works_at',
  object: { type: 'organization', name: 'Anthropic' },
  validFrom: Date.parse('2026-02-01T00:00:00Z'),
  provenance: { source: 'conversation' },
});

// May: David moves to OpenAI. works_at is single-valued, so this supersedes
// the February fact: GapTime closes the Anthropic fact at May 1 and records a
// contradiction. Nothing is deleted.
memory.assert({
  subject: { type: 'person', name: 'David' },
  predicate: 'works_at',
  object: { type: 'organization', name: 'OpenAI' },
  validFrom: Date.parse('2026-05-01T00:00:00Z'),
  provenance: { source: 'conversation' },
});

// Valid-time travel: where did David work at each instant?
memory.asOf(Date.parse('2026-03-15T00:00:00Z')); // -> Anthropic
memory.asOf(Date.parse('2026-06-15T00:00:00Z')); // -> OpenAI

// Transaction-time travel: what did we BELIEVE in March, before the May update?
memory.reconstructAsOf(Date.parse('2026-03-15T00:00:00Z')); // -> Anthropic, open interval

Every predicate is single-valued by default (one current object per subject and predicate). For genuinely set-valued relations (likes, knows, owns), register them as { cardinality: 'multi' } or pass defaultCardinality: 'multi', otherwise a second object supersedes the first.

The full engine surface is assert, ingest, retract, query, asOf, reconstructAsOf, history, neighbors, search, contradictions, entity, entities, resolveEntity, registerPredicate, facts, prune, stats, auditTrail, on, flush, and close, specified in [SPEC.md](./SPEC.md).


The two time axes and the query surface

Every fact is a statement subject predicate object annotated with two half-open intervals: valid time [validFrom, validTo) (when it was true in the world, open end means still valid) and transaction time [txFrom, txTo) (when GapTime believed it, open end means current belief). Reads default to "valid now, believe now"; pass either coordinate to time-travel on that axis independently, or both for a true bi-temporal question.

| Query | What it answers | |---|---| | asOf(t) | Facts true at valid-time t, believed now. The "what was true then" axis. | | reconstructAsOf(t) | Facts believed at transaction-time t, regardless of later change. The "what did the system know then" axis. | | query(pattern, { asOfValidTime, asOfTransactionTime }) | Both axes independently, with a triple-position filter. | | history(pattern) | Every version ever written, nothing filtered. The raw log behind time-travel. | | neighbors(id, { depth, asOfValidTime }) | Graph traversal outward from an entity, honoring valid time so only live edges are walked. | | search(text, { asOfValidTime, expandHops }) | Hybrid lexical, graph, and optional vector retrieval, filtered to the chosen time. | | auditTrail({ since, until }) | The transaction-time axis as a frozen, append-only event log. |


Contradiction detection

A single knob, predicate cardinality, drives contradiction detection. For a single-valued predicate, asserting a different object over an overlapping valid interval is a contradiction; GapTime classifies the overlap with the thirteen Allen interval relations, closes the older fact over only the contradicted span, and keeps any surviving prefix and suffix. The relation is recorded so you can see exactly how two facts collided.

| Relation group | Members | Meaning for two valid intervals | |---|---|---| | disjoint | before, after | No overlap; never a contradiction | | touching | meets, metBy | Adjacent at a single instant; half-open, so no overlap | | partial overlap | overlaps, overlappedBy | Genuine overlap; a contradiction for a single-valued predicate | | containment | during, contains | One interval inside the other; the surviving prefix and suffix are kept | | boundary-shared | starts, startedBy, finishes, finishedBy | Share one endpoint and overlap | | identical | equals | Same interval; the newer fact fully supersedes the older |

Same-object overlapping assertions are restatements, not contradictions: they merge into the union interval so a subject never carries two current versions of the same object at one instant. Disjoint same-object spans (true Monday, true Friday, not in between) stay separate.


The bi-temporal model

GapTime never deletes. Two operations close intervals, and both stay reconstructable, which is what keeps time-travel exact rather than approximate.

supersession  closes the older fact's VALID interval over the contradicted span,
              writes clipped current-belief versions for the surviving prefix and
              suffix, and ends the older belief on the transaction axis.

retraction    closes a fact's TRANSACTION interval ("we recorded this in error"),
              and heals the supersession it caused: the overridden beliefs are
              restored, the clips are closed. reconstructAsOf at a past instant is
              unaffected, because every prior version is still in history.

The worked example, David moving from Anthropic to OpenAI, shows the rule: asserting OpenAI valid from May 1 over Anthropic valid from February closes Anthropic at May 1 (the finishedBy relation when both ran open), so asOf(March) is Anthropic, asOf(June) is OpenAI, and reconstructAsOf(before the May write) still returns Anthropic with an open interval, exactly what the system believed then.

Retrieval

search fuses three signals by reciprocal rank fusion, filtered to the requested time coordinates: an in-memory Okapi BM25 lexical index over an inverted postings map, n-hop graph traversal around the lexical hits, and an optional pluggable vector hook. The lexical and graph signals work with no database and no model; semantic recall is opt-in through a vector: VectorIndex you supply.

Ingesting MCP tool output

An MCP tool result is an ideal fact source: it carries structuredContent, a real-world timestamp, and a provenance hint. GapTime maps the result timestamp to valid time and the ingestion instant to transaction time, keeping the tool as the provenance source so any answer traces back to the exact call that produced it:

import { ingestMcpResult } from '@takk/gaptime/mcp';

await ingestMcpResult(memory, 'web_search', toolResult, {
  facts: (result) => toFacts(result.structuredContent),
});

Keep the fact mapping deterministic, or let the configured Extractor produce facts; either way GapTime never calls the model itself. The runnable version is [examples/mcp-tools-ingest.ts](./examples/mcp-tools-ingest.ts).


Persistence

State backends persist entities, every fact version, episodes, contradictions, the audit trail, and the supersession ledger, so a reload restores the whole store. Three ship in the box: memoryState (default), fileState (Node, atomic unique-temp writes with corrupt-snapshot quarantine), and kvState, which wraps any string key-value client in one line:

import { createGapTime, kvState } from '@takk/gaptime';

// Any Redis client (ioredis, node-redis), one line:
const state = kvState({
  get: (k) => redis.get(k),
  set: (k, v) => redis.set(k, v),
});

// Upstash Redis:   get: (k) => upstash.get(k), set: (k, v) => upstash.set(k, v)
// Cloudflare KV:   get: (k) => env.GAPTIME_KV.get(k),  set: (k, v) => env.GAPTIME_KV.put(k, v)

const memory = createGapTime({ state });
// ... assert and query as usual ...
await memory.flush(); // snapshot saved; a new engine with the same backend restores it

GapTime keeps its working set in memory: the graph lives in the heap (roughly 2 kB per fact version), nothing is freed automatically, and maxFacts (default 100000) is a hard ceiling, not an eviction policy. For long-running agents, apply a retention cutoff with prune(beforeTransactionTime), which drops fully-closed history and audit events before the cutoff while keeping every current belief; choose a cutoff that respects your retention obligations. For datasets beyond a bounded working set, mirror writes into a real database with the @takk/gaptime/store adapters. GapTime is a fast embeddable temporal layer for the working set, not a database replacement at large scale.


Vercel AI SDK ingestion

The AI SDK surfaces each agent step through onStepFinish and onFinish. GapTime turns the tool results into facts, mapping the response timestamp to valid time and the ingestion instant to transaction time:

import { createGapTime } from '@takk/gaptime';
import { gaptimeOnStepFinish } from '@takk/gaptime/vercel';

const memory = createGapTime();

const result = await generateText({
  model,
  prompt,
  onStepFinish: gaptimeOnStepFinish(memory, { facts: stepToFacts }),
});

The adapter is structural: it matches the AI SDK step shape without importing the ai package, so the zero-dependency invariant survives. A host-supplied facts mapper declares which facts each step yields.


OpenTelemetry ingestion

If your stack already emits GenAI spans, GapTime ingests them directly. ingestSpan reads span attributes and timing, maps the span start time to valid time and the ingestion instant to transaction time, and uses the span's tool or operation as the provenance source:

import { createGapTime } from '@takk/gaptime';
import { ingestSpan, type GenAiSpan } from '@takk/gaptime/otel';

const memory = createGapTime();

// Inside a span processor's onEnd, an OTLP collector hook, or wherever
// finished spans surface in your host:
async function onSpanEnd(span: GenAiSpan): Promise {
  await ingestSpan(memory, span, { facts: spanToFacts });
}

It tolerates the HrTime tuple, epoch-millisecond, and Date start-time spellings in circulation, and works with the spans the Vercel AI SDK emits under experimental_telemetry.


CLI and the serve bridge

The gaptime binary ships these commands: help, version, assert, ingest, query, asof, reconstruct, contradictions, audit, stats, search, demo (the deterministic demonstration above), inspect (print a saved snapshot, --watch for a live redraw), and serve.

gaptime serve wraps one engine in a hardened local HTTP bridge so non-JavaScript hosts can assert, query, time-travel, and read statistics. It binds loopback by default, refuses non-loopback hosts without a bearer token, rejects non-loopback Host headers in tokenless mode with 403 (the DNS-rebinding defense, /healthz included), compares tokens in constant time, and gates bodies with 415 and 413 responses.

gaptime serve --port 4378 --token "$GAPTIME_TOKEN" --state .gaptime/state.json

curl -s -X POST http://127.0.0.1:4378/assert \
  -H "authorization: Bearer $GAPTIME_TOKEN" \
  -H "content-type: application/json" \
  -d '{
    "subject": { "type": "person", "name": "David" },
    "predicate": "works_at",
    "object": { "type": "organization", "name": "OpenAI" },
    "validFrom": 1777593600000,
    "provenance": { "source": "conversation" }
  }'

Hermes Agent note: a Hermes deployment can call /ingest after each tool result and /asof or /reconstruct when it needs context, getting bi-temporal memory and an audit history without touching its own runtime. The full recipe, including the shell hook and the honest limits of an out-of-process bridge, is in [examples/hermes-bridge.md](./examples/hermes-bridge.md).


Package surface

| Entry | What it is | Brotli size | |---|---|---| | @takk/gaptime | The engine, intervals, retrieval, state backends, hashing | 6.18 kB ESM / 6.37 kB CJS | | @takk/gaptime/otel | OpenTelemetry GenAI span ingestion | 404 B | | @takk/gaptime/vercel | Vercel AI SDK ingestion | 372 B | | @takk/gaptime/mcp | Model Context Protocol ingestion | 362 B | | @takk/gaptime/integrations | The five family bridges | 575 B | | @takk/gaptime/store | Structural Neo4j, Postgres, SQLite adapters | under 1 kB | | @takk/gaptime/web | Browser surface, no Node imports |

Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.