# Gaptime

> 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.

- **Type:** MCP server
- **Install:** `agentstack add mcp-davccavalcante-gaptime`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [davccavalcante](https://agentstack.voostack.com/s/davccavalcante)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [davccavalcante](https://github.com/davccavalcante)
- **Source:** https://github.com/davccavalcante/gaptime
- **Website:** https://davccavalcante.github.io/gaptime/

## Install

```sh
agentstack add mcp-davccavalcante-gaptime
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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:

```console
$ 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

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

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

```ts
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.

```text
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:

```ts
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:

```ts
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:

```ts
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:

```ts
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.

```bash
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.

- **Author:** [davccavalcante](https://github.com/davccavalcante)
- **Source:** [davccavalcante/gaptime](https://github.com/davccavalcante/gaptime)
- **License:** Apache-2.0
- **Homepage:** https://davccavalcante.github.io/gaptime/

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

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-davccavalcante-gaptime
- Seller: https://agentstack.voostack.com/s/davccavalcante
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
