Skip to content

Native note revision history across cloud and local projects #1156

Description

@phernandez

Summary

Add native, backend-aware revision history for Basic Memory notes.

  • Cloud projects: use the object versions and point-in-time snapshots already provided by Tigris.
  • Local projects: use Git or Jujutsu as the revision store.
  • MCP/API: expose one revision-aware contract for historical reads, file history, comparison, and safe restoration.

This preserves the useful goals from #1154—visibility into changes, diff, and undo—without recording a second mutation log in the lifecycle-hook inbox from #997.

This is new product work. Basic Memory does not currently have the cross-backend revision abstraction, a local Git/JJ history backend, or revision-aware MCP tools. Existing database versions and checksums are synchronization and optimistic-concurrency state; they are not retained note history.

Why native revisions instead of mutation envelopes

A durable mutation envelope duplicates information already represented by versioned file content and creates another source of truth that must be retained, queried, repaired, and reconciled.

It is also incomplete for a local-first product: an envelope emitted from Basic Memory's write path cannot see edits made directly through Obsidian, VS Code, shell scripts, file synchronization, or other tools. A storage-backed revision source can cover both Basic Memory writes and externally edited files once the local watcher observes them.

The #997 inbox has a different job. It captures selected Claude/Codex lifecycle events with redaction, opt-in configuration, idempotency, and bounded operational concerns. It should not become the authoritative, permanent history of note content.

Revision history should answer:

  • What did this note contain at a particular revision or time?
  • What changed between two revisions?
  • Which files were added, removed, or modified across a directory or project?
  • Can I restore old content without erasing later history?

Actor trust, write authorization, and lifecycle telemetry are separate concerns and are not part of this issue.

Proposed architecture

Define a narrow revision-source capability in core and provide runtime-specific implementations:

MCP tools / CLI
       ↓
Typed API clients
       ↓
API and note services
       ↓
NoteRevisionSource protocol
       ├── TigrisRevisionSource (cloud)
       └── Git/JJRevisionSource (local)

The contract should model capabilities, not provider internals. A revision reference is opaque to callers but carries enough scope information for the provider to validate it.

Conceptually, the source needs to support:

read_at(path, revision)
history(path, cursor, limit)
compare(scope, from_revision, to_revision)
restore(path, revision, expected_current_revision)

The exact Python and public MCP schemas should be reviewed before implementation. The important boundary is that core owns the behavior and response models while cloud/local composition roots provide the storage implementation.

Unsupported capabilities must fail clearly—for example, revision history is not available for this project—rather than silently returning current content or an empty history.

Revision reference semantics

A revision reference may identify:

  • an exact stored object/file revision;
  • a project-wide snapshot/commit; or
  • a timestamp resolved to the latest revision at or before that time.

The response should return the resolved, provider-issued revision identifier so callers can use a stable ref for subsequent reads or comparisons.

Provider-specific identifiers remain opaque:

  • Tigris exact object version IDs apply to a single path.
  • Tigris snapshot timestamps apply to notes, prefixes, and whole projects.
  • Git/JJ commit IDs apply to a project tree and can be narrowed to a path or prefix.

Timestamp resolution must be explicit and deterministic: each path resolves to its latest stored revision at or before the requested instant.

Cloud implementation: Tigris

Tigris snapshot-enabled buckets already retain historical object content. The cloud implementation should use that history directly instead of creating shadow revision rows or mutation records.

Historical reads

For one note or file:

  1. Resolve the identifier to its current path when necessary.
  2. Read the requested Tigris object version or the version at-or-before a timestamp.
  3. Parse the historical Markdown/file content directly.
  4. Return the resolved revision metadata with the content.

The current knowledge index describes now. Historical reads must not pretend that the current indexed observations and relations represent the historical graph.

File history

Return a cursor-paginated list containing at least:

  • revision/version ID;
  • timestamp;
  • size;
  • etag or checksum;
  • whether it is the latest revision;
  • deletion state when the provider exposes it.

Presentation layers may group rapid saves, but storage should preserve the original revisions. Coalescing is a UI concern unless a later retention policy says otherwise.

Recursive compare

A directory or project comparison can be computed cheaply from two at-version listings:

  1. List the selected prefix at from_revision.
  2. List it at to_revision.
  3. Join entries by path.
  4. Classify added, removed, and modified files; use etag/checksum inequality for modification detection.
  5. Fetch file bodies only when a caller asks for a per-file text or semantic diff.

Example manifest:

scope: project | prefix | note
from_revision: ...
to_revision: ...
added:
  - path: notes/new.md
    to_revision: ...
    size: 2048
removed:
  - path: notes/old.md
    from_revision: ...
modified:
  - path: notes/project.md
    from_revision: ...
    to_revision: ...
    from_etag: ...
    to_etag: ...

Restore

Restoring historical content must read the old revision and write it through the normal current write path. The restoration therefore creates a new head revision and never deletes or rewinds the intervening history.

The write should accept an expected current revision/checksum so a restore cannot silently overwrite a newer concurrent edit. A named snapshot before a bulk restore may provide an additional safety marker, but the underlying object history remains authoritative.

Local implementation: Git or Jujutsu

Local projects need a real revision source rather than emulated empty history.

The implementation must:

  • record Basic Memory writes;
  • record direct filesystem edits after the project watcher observes them;
  • preserve revisions without requiring GitHub or any remote;
  • avoid changing the user's active branch, staging area, or normal commit history without explicit opt-in;
  • expose stable revision IDs and timestamps through the same core contract;
  • remain recoverable with standard Git/JJ tooling;
  • fail loudly if history is disabled or unavailable.

Candidate designs

  1. Git object database with a Basic Memory-owned ref

    • Create trees/commits through Git plumbing.
    • Advance a namespaced ref such as refs/basic-memory/history/<project-id>.
    • Do not checkout, stage, or commit onto the user's branch.
  2. Sidecar bare Git repository

    • Store the revision database under Basic Memory's application data rather than inside the note project.
    • Avoid interfering with an existing repository and support projects that are not already Git repositories.
  3. Jujutsu-backed history

    • Use JJ's automatic working-copy snapshots and Git-compatible storage.
    • Evaluate the operational cost of requiring or bundling the jj executable.

The first implementation should be chosen by a focused spike. Git is more universally available; JJ may offer better automatic snapshot and conflict semantics. The core contract must not depend on that choice.

Revision boundaries

Git and JJ do not make every filesystem write a useful user-facing revision automatically. The local backend must define when the watcher records a new revision and how rapid edits are grouped.

Possible boundaries include:

  • after an accepted Basic Memory write;
  • after a watcher batch settles;
  • after a short debounce window;
  • before and after bulk operations;
  • an explicit user checkpoint.

This is revision capture, not semantic mutation logging: the stored artifact is the complete file/project state represented by the version-control backend.

Proposed MCP/API surface

Candidate MCP operations:

read_note(identifier, revision=...)
read_content(path, revision=...)
file_history(identifier, cursor=..., limit=...)
compare(scope, from_revision, to_revision)
restore_note(identifier, revision, expected_current_revision=...)

Candidate API routes:

GET  /{project}/notes/{identifier}?revision={ref}
GET  /{project}/notes/{identifier}/revisions?cursor=&limit=
GET  /{project}/compare?from={ref}&to={ref}&prefix={path}
GET  /{project}/compare/file?path=&from=&to=&format=text|semantic
POST /{project}/notes/{identifier}/restore

Exact names are subject to contract review. Responses should use backend-neutral revision terminology even when the underlying provider calls the identifier a version, snapshot, or commit.

search_notes and build_context remain current-index operations in the first version. Adding historical search or a historical knowledge graph would be a separate, substantially larger feature.

Moves, deletes, and identity

Both Tigris and Git naturally organize history by path:

  • a move may initially appear as one removed path and one added path;
  • Git rename detection is heuristic;
  • Tigris object versions belong to object keys;
  • deleted files may only be discoverable through delete markers or historical tree listings.

The first implementation may offer honest path-based history. Following a note across moves requires a stable note identity that survives historical parsing and should be designed explicitly rather than inferred differently by each backend.

Live updates

A live view of incoming changes can be built later from Tigris object notifications and the local filesystem watcher. That stream may be ephemeral and operational; it does not need to become a second durable content history.

Non-goals

  • Persisting knowledge mutations in the Capture Claude/Codex harness events as Basic Memory producer envelopes #997 lifecycle inbox.
  • Per-agent trust tiers or automatic write authorization.
  • Authenticated attribution for every revision.
  • Branch, merge, or pull-request workflows for notes.
  • GitHub push/pull synchronization.
  • Historical search or historical build_context.
  • Reconstructing a complete historical knowledge graph from the current index.
  • Silently falling back to current content when a revision cannot be resolved.

Delivery phases

  1. Core contract and cloud historical reads

    • Define revision refs and response models.
    • Add the revision-source protocol.
    • Implement Tigris-backed read_note, read_content, and file history.
  2. Compare

    • Add note/prefix/project manifests.
    • Add on-demand per-file text diffs.
    • Leave semantic diff as an additive renderer.
  3. Local backend

    • Spike Git owned-ref, sidecar Git, and JJ approaches.
    • Implement the selected backend and watcher revision boundaries.
    • Validate that user branches and indexes are untouched.
  4. Safe restore

    • Restore old content through the current write path.
    • Preserve history and enforce an optimistic-concurrency precondition.

Delivery should use small PRs even if this issue remains the public architectural tracker.

Acceptance criteria

  • Core exposes a narrow, backend-neutral revision-source contract.
  • Cloud history uses Tigris object/snapshot history without duplicating note revisions in the database or hook inbox.
  • Local history uses Git or JJ and does not require a remote repository.
  • Local external edits become revisions after watcher settlement.
  • Historical note/file reads return historical content and resolved revision metadata.
  • File history is paginated and returns stable revision identifiers.
  • Compare supports note, directory/prefix, and project scopes.
  • Per-file text diff is fetched on demand rather than for every manifest entry.
  • Restore writes a new head revision and rejects stale concurrent restores.
  • The current-only nature of search, build_context, observations, and relations is documented in tool descriptions.
  • Unsupported history fails explicitly; there is no silent current-content or empty-history fallback.
  • Provider contract, cloud adapter, local adapter, MCP/API, and restore behavior have regression tests.

Open questions

  • Should local history reuse an existing Git repository or always use a Basic Memory-owned sidecar/namespace?
  • Is Git plumbing sufficient, or is JJ's working-copy model worth the additional dependency?
  • What watcher debounce/coalescing policy produces useful revisions without excessive noise?
  • What stable identity should connect history across moves?
  • How should retention and compaction differ between Tigris and local Git/JJ history?
  • Should public parameters be named revision everywhere, or retain version where users already recognize it?

Related work

Metadata

Metadata

Assignees

No one assigned

    Labels

    cloudBasic Memory CloudenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions