Point in Time

Every state the retained history covers can be brought back: read-only, as a new store, or as the store's own state again.

Targets

TargetRust (RecoveryTarget)CLIMeans
the endLatest(no option)the current state
a commitCommitSeq(n)--at-commit <N>right after commit N
a timeTime(micros)--at-time <TIME>after the last commit at or before TIME
a markMark(name)--at-mark <NAME>at the mark (right after the commit it points at)

Every commit is a target, not only snapshots and marks. A target is resolved the same way for every action: the nearest snapshot at or before it is loaded and the WAL is replayed up to the target, following the lineage chain across forks and in-place rollbacks. A target needs the snapshot and WAL that lead to it: after a prune, targets before the oldest kept snapshot are gone (PersistError::TargetNotReached).

The time format

TIME is an RFC 3339 / ISO 8601 date-time with a zone, or microseconds since the Unix epoch:

2026-10-08T02:43:00Z          UTC
2026-10-08T02:43Z             seconds may be left out
2026-10-08T04:43:00+02:00     an offset
2026-10-08T02:43:00.250Z      a fraction of a second
1760000000000000              microseconds since 1970-01-01T00:00:00Z
  • A time without a zone is refused (it would depend on the machine's time zone), and so is a bare date: a day is not a moment, and whether its start or its end is meant, in which zone, changes the target. The error shows the explicit forms (the same text in the CLI and the dev server's {"time": ..}):

    --at-time: "2026-10-08" is a date without a time and a zone, which is refused (it names no moment): write 2026-10-08T23:59:59Z for the end of that day in UTC, or with an offset, e.g. 2026-10-08T00:00:00+02:00 for its start at UTC+02:00 (RFC 3339)
    

    Python takes a time only as microseconds since the epoch (time=), so no text is parsed there.

  • A time covers the whole second (or minute) it names: 2026-10-08T02:43Z means up to 02:43:59.999999. So a time copied from store list or store marks, which print whole seconds in this format, includes the commit printed with it.

  • Times are printed in UTC (2026-10-08T07:05:08Z). The dev server's Go to time... takes a date and time in the browser's time zone.

What you can do with a target

ActionLibraryCLIDev server (Store menu)
look at it, read-onlyStore::open_read_only(dir, target), store.read_only_at(target)fork it, then open the forkOpen read-only here
branch it into a new storestore.fork(target, new_dir)graphersal store fork <DIR> <NEW_DIR> TARGETFork to a new directory...
take THIS store back to itstore.rollback_to(target, mode)graphersal store rollback <DIR> TARGETRoll back here...

Read-only views

#![allow(unused)]
fn main() {
use graphersal::persist::{RecoveryTarget, Store};

let past = Store::open_read_only("data", RecoveryTarget::CommitSeq(12))?;
println!("commit {}: {} vertices", past.report.commit_seq, past.graph.vertex_count());
let mut graph = past.graph;                  // an unpublished TraversalGraph: query it, export it
let names = graph.traversal_mut().v(None::<()>).values("name").to_list()?;
Ok::<(), Box<dyn std::error::Error>>(())
}

Store::open_read_only takes no lock and works while a writer has the store open, also on a backup. It returns the graph with a RecoveryReport; the graph has no journal, so changes to it are not stored anywhere (attach a journal only as a new history: a new lineage). A commit after the end of the history, or a mark that does not exist, is an error (PersistError::TargetNotReached); a time after the last commit is the latest state.

In the dev server, Open read-only here makes the server serve that state instead of the live graph: queries, the drawing, the schema and the statistics show it, every write is refused ("the served state is a read-only view at commit N ...: writes are refused"; its help points to Back to the live graph and to a fork), and a banner offers Back to the live graph. The view is shared by every browser tab and the MCP agent. See Dev Server and MCP.