Clients
A TypeScript and a Python client for chronos serve, one file each, no dependencies. They speak the HTTP API (POST /v1/<op>), not the Postgres port: for Postgres drivers (psql, pg, psycopg, JDBC, ...) see Postgres compatibility.
const db = new Chronos("http://127.0.0.1:7070", { token: process.env.CHRONOS_TOKEN });
const agent = await db.fork("agent-7"); // instant copy of main
await agent.put("users", "1", { name: "Ada L." });
console.log(await agent.diff());
await agent.merge(); // or agent.discard()
Getting them
TypeScript/JavaScript, from npm (@chronosdb/client, ESM, with its types):
npm install @chronosdb/client
Python isn't on PyPI yet: copy the file into your project.
| Language | Get it | Needs |
|---|---|---|
| TypeScript / JavaScript | npm install @chronosdb/client, or copy clients/typescript/chronos.ts | global fetch: Node 18+, browsers, edge runtimes |
| Python | clients/python/chronos.py | Python 3.9+, standard library only |
import { Chronos, ChronosError } from "@chronosdb/client";
from chronos import Chronos, ChronosError
Constructor
new Chronos(url = "http://127.0.0.1:7070", { token?: string, branch?: string } = {})
Chronos(url="http://127.0.0.1:7070", token=None, branch="main")
| Option | Default | |
|---|---|---|
url | http://127.0.0.1:7070 | the server; a trailing / is dropped |
token | none | sent as Authorization: Bearer <token>: the server's token, an agent's token or the admin token (see auth) |
branch | main | the branch every call works on; also name@when (see at) |
- No timeouts or retries: neither client sets one. Calls wait as long as
fetch/urllib.request.urlopendo; wrap them (anAbortSignalisn't accepted) if you need a limit. - A client is one branch.
fork,branchandatreturn a new client; the old one is unchanged. - Its branch:
db.branchName(TypeScript, read only),db.branch_name(Python).
Methods
TypeScript methods return promises. Python names are snake_case: setMeta is set_meta, undoMerge is undo_merge, vectorField is vector_field, onlyTables is only_tables, onlyKeys is only_keys, skipChanged is skip_changed. Each maps to one HTTP op on the client's branch.
| Method | Op | Returns |
|---|---|---|
get(table, id) | get | the row, or null / None |
put(table, id, record) | put | nothing |
delete(table, id) | delete | nothing |
batch(rows) | batch | rows written |
find(table, options) | find | {total, hits, nextOffset} |
sql(sql, params) | sql | the last statement's {command, rows} |
fork(name, meta?) | fork | a client on the new branch |
branch(name) | none | a client on name |
branches() | branches | the branches |
world() | world | this branch |
setMeta(meta) | set_meta | this branch |
diff() | diff | the changes |
discard() | discard | nothing |
merge(resolve, options) | merge | changes merged |
preview(resolve, options) | merge with dry_run | the plan |
undoMerge(world, options) | undo_merge | what was put back |
at(when) | none | a client reading the past |
history(limit = 100) | history | events, newest first |
restore(at) | restore | rows written |
simulate(worlds, prefix, script, score, options) | simulate | the worlds, best first |
replay(world, as?) | replay | whether it came out identical |
Rows
| Method | Arguments | Returns |
|---|---|---|
get(table, id) | strings | the row object; null / None when there is no such row. Other 404s (no such branch) throw |
put(table, id, record) | record: an object, the whole row | nothing |
delete(table, id) | nothing; a missing row throws 404 | |
batch(rows) | [{table, id, record}]; record: null / None deletes | the count. All rows or none |
put, delete, batch and a writing sql remember the branch's version, which merge sends (see merge).
find
find(table, { where?, text?, vector?, vectorField?, limit?, offset? })
find(table, where=None, text=None, vector=None, vector_field=None, limit=None, offset=None)
| Option | Default | |
|---|---|---|
where | none | {field: value}, exact match on top-level fields, all must match |
text | none | BM25 over the rows' string fields |
vector + vectorField | none | rank by dot product with the vectors in that field; give both |
limit | 20 (server) | |
offset | 0 |
Returns { total, hits: [{ id, score, row }], nextOffset } in TypeScript, {"total", "hits", "next_offset"} in Python. total counts rows passing the filters; pass nextOffset as offset for the next page (null / None on the last). The clients don't send ef. See search.
sql
sql(sql, params = []) // { command: string, rows: R[] }
sql(sql, params=None) # {"command": ..., "columns": [...], "rows": [...]}
Runs one or more statements on the client's branch; params fill $1, $2, .... Returns only the last statement's result: command is its tag (INSERT 0 1, SELECT 2), rows are objects keyed by column name (the reply also carries columns, the names in order). Nothing ran: {command: "", rows: []}. Each call is its own session: BEGIN ... COMMIT must be within one call. Errors: status 400, 403 (42501), 404 (3D000, 42P01), 409 (23xxx), 413, 429, 500. See SQL.
Worlds
| Method | Arguments | Returns |
|---|---|---|
fork(name, meta?) | meta: notes kept with the branch | a client on name, forked from this client's branch |
branch(name) | a client on name (no request) | |
branches() | [World], _tx_ worlds left out | |
world() | this branch's World | |
setMeta(meta) | an object merged into the metadata; null removes a key | the branch's World |
diff() | [{ key, before, after, columns }]: before null for an insert, after null for a delete | |
discard() | nothing |
A World is { name, id, parent, depth, created, version, flagged, meta, owner, expires, active, pinned, checkpoints } (created in ms since 1970; checkpoints [{name, at}]). The TypeScript World type declares the first eight.
The clients don't send diff's to or sql, or discard's cascade: use sql("diff world ...") or the HTTP API for those.
Merge
merge(resolve = "fail", { columns?, picks?, into?, onlyTables?, onlyKeys?, confirm? })
preview(resolve = "fail", { columns?, picks?, into?, onlyTables?, onlyKeys? })
undoMerge(world, { skipChanged? })
merge(resolve="fail", confirm=False, columns=False, picks=None, into=None, only_tables=None, only_keys=None)
preview(resolve="fail", columns=False, picks=None, into=None, only_tables=None, only_keys=None)
undo_merge(world, skip_changed=False)
| Option | Default | |
|---|---|---|
resolve | fail | on conflict: fail, ours (this branch's values) or theirs (the parent's) |
columns | false | rows both sides changed merge column by column; only a column both changed conflicts |
picks | none | {"table/id": "ours" | "theirs" | {row} | null}; null / None deletes |
into | the parent | merge into another branch; this branch stays |
onlyTables | all | merge only these tables; this branch stays |
onlyKeys | all | merge only these rows ("table/id"); this branch stays |
confirm | false | after a crash, merge the flagged branch as it is now (check diff() first) |
mergeapplies this branch to its parent and deletes it (unlessinto,onlyTablesoronlyKeys). Returns the number of changes merged.- Version check:
mergesends the version from this client's last write (0 right afterfork), so writes lost in a server crash fail with 409 instead of merging silently. A client made withbranch(name)that hasn't written sends none.confirm: truesends none and takes the branch as it is. - Conflicts:
ChronosErrorwith status 409 andconflicts:[{ key, base, ours, theirs, explain }]. Nothing was merged. previewchanges nothing and returns{ rows: [{ key, outcome, detail, base, ours, theirs, result }], conflicts, blocked }:conflictscounts them,blockedis why constraints would refuse the merge, ornull. Outcomes: MERGE DRY RUN.undoMerge(world)puts back, in the parent, every rowworld's latest merge changed. Returns{ ok, parent, at, undone, skipped }. Rows changed since fail it (409) unlessskipChanged.
Reading the past
| Method | Arguments | Returns |
|---|---|---|
at(when) | a time ("2026-09-20 10:00") or a time ago ("-5 minutes") | a client on <branch>@<when>: reads the past, writes fail |
history(limit = 100) | [{ at, world, event, rows }], newest first | |
restore(at) | a time, as for at | rows written: the branch put back as it was, as an ordinary write |
See worlds for what when can be.
Simulate
simulate(worlds, prefix, script, score, { keep?, order?, seed?, threads? })
replay(world, as?)
simulate(worlds, prefix, script, score, keep=None, order="desc", seed=0, threads=None)
replay(world, as_name=None)
| Argument | Default | |
|---|---|---|
worlds | how many, more than 0 | |
prefix | world i is named <prefix>_<i> | |
script | SQL run in each world; $1 is its index, $2 its seed | |
score | a SELECT giving one number per world | |
keep | all | worlds to keep, best first, or "all" |
order | desc | desc: highest score best; asc: lowest |
seed | 0 | seeds random() in every world |
threads | every free core |
Forks from this client's branch at one moment. simulate returns { worlds: [{ world, id, index, seed, score, error, kept }], base, at, timings: { fork_ms, run_ms, discard_ms, total_ms } }, best first. replay returns { world, replay, identical, rows_differing, score, recorded_score, error }; as / as_name keeps the replay as a new branch (replay is its name, else null). See SIMULATE.
Errors
Every non-2xx reply throws ChronosError:
| Field | TypeScript | Python | |
|---|---|---|---|
| message | e.message | str(e) | the server's message, which says what to fix |
| status | e.status | e.status | 400, 401, 403, 404, 409, 413, 429, 500 (see HTTP API) |
| conflicts | e.conflicts | e.conflicts | a merge's conflicting rows; [] otherwise |
- No SQLSTATE: the server's
sqlerrors carrycode(e.g.42P01), butChronosErrorkeeps onlystatus,messageandconflicts. Use a Postgres driver when you need the code. - Not
ChronosError: a network error (TypeErrorfromfetch,urllib.error.URLError) or a reply that isn't JSON (SyntaxError,json.JSONDecodeError) throws the runtime's own error.
Examples
TypeScript: fork, change, check, merge, retrying on a conflict.
import { Chronos, ChronosError } from "./chronos";
const db = new Chronos("http://127.0.0.1:7070", { token: process.env.CHRONOS_TOKEN });
const agent = await db.fork("agent-7", { task: "fix names" });
await agent.put("users", "1", { name: "Ada L." });
const { rows } = await agent.sql("select count(*) as n from users");
console.log(rows[0].n, await agent.diff());
try {
await agent.merge();
} catch (e) {
if (!(e instanceof ChronosError) || e.status !== 409) throw e;
console.log(e.conflicts.map((c) => c.key));
await agent.merge("ours");
}
Python: search, then write on a fork.
import os
from chronos import Chronos, ChronosError
db = Chronos("http://127.0.0.1:7070", token=os.environ.get("CHRONOS_TOKEN"))
found = db.find("notes", where={"city": "blr"}, text="office", limit=5)
agent = db.fork("agent-8")
for hit in found["hits"]:
agent.put("notes", hit["id"], {**hit["row"], "checked": True})
print(agent.preview()["conflicts"])
agent.merge()
More: examples/agents (tool definitions for LLM APIs, 20 agents merging at once), run in CI by tests/clients.rs.