Skip to main content

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.

LanguageGet itNeeds
TypeScript / JavaScriptnpm install @chronosdb/client, or copy clients/typescript/chronos.tsglobal fetch: Node 18+, browsers, edge runtimes
Pythonclients/python/chronos.pyPython 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")
OptionDefault
urlhttp://127.0.0.1:7070the server; a trailing / is dropped
tokennonesent as Authorization: Bearer <token>: the server's token, an agent's token or the admin token (see auth)
branchmainthe 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.urlopen do; wrap them (an AbortSignal isn't accepted) if you need a limit.
  • A client is one branch. fork, branch and at return 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.

MethodOpReturns
get(table, id)getthe row, or null / None
put(table, id, record)putnothing
delete(table, id)deletenothing
batch(rows)batchrows written
find(table, options)find{total, hits, nextOffset}
sql(sql, params)sqlthe last statement's {command, rows}
fork(name, meta?)forka client on the new branch
branch(name)nonea client on name
branches()branchesthe branches
world()worldthis branch
setMeta(meta)set_metathis branch
diff()diffthe changes
discard()discardnothing
merge(resolve, options)mergechanges merged
preview(resolve, options)merge with dry_runthe plan
undoMerge(world, options)undo_mergewhat was put back
at(when)nonea client reading the past
history(limit = 100)historyevents, newest first
restore(at)restorerows written
simulate(worlds, prefix, script, score, options)simulatethe worlds, best first
replay(world, as?)replaywhether it came out identical

Rows​

MethodArgumentsReturns
get(table, id)stringsthe 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 rownothing
delete(table, id)nothing; a missing row throws 404
batch(rows)[{table, id, record}]; record: null / None deletesthe 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)
OptionDefault
wherenone{field: value}, exact match on top-level fields, all must match
textnoneBM25 over the rows' string fields
vector + vectorFieldnonerank by dot product with the vectors in that field; give both
limit20 (server)
offset0

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​

MethodArgumentsReturns
fork(name, meta?)meta: notes kept with the brancha 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 keythe 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)
OptionDefault
resolvefailon conflict: fail, ours (this branch's values) or theirs (the parent's)
columnsfalserows both sides changed merge column by column; only a column both changed conflicts
picksnone{"table/id": "ours" | "theirs" | {row} | null}; null / None deletes
intothe parentmerge into another branch; this branch stays
onlyTablesallmerge only these tables; this branch stays
onlyKeysallmerge only these rows ("table/id"); this branch stays
confirmfalseafter a crash, merge the flagged branch as it is now (check diff() first)
  • merge applies this branch to its parent and deletes it (unless into, onlyTables or onlyKeys). Returns the number of changes merged.
  • Version check: merge sends the version from this client's last write (0 right after fork), so writes lost in a server crash fail with 409 instead of merging silently. A client made with branch(name) that hasn't written sends none. confirm: true sends none and takes the branch as it is.
  • Conflicts: ChronosError with status 409 and conflicts: [{ key, base, ours, theirs, explain }]. Nothing was merged.
  • preview changes nothing and returns { rows: [{ key, outcome, detail, base, ours, theirs, result }], conflicts, blocked }: conflicts counts them, blocked is why constraints would refuse the merge, or null. Outcomes: MERGE DRY RUN.
  • undoMerge(world) puts back, in the parent, every row world's latest merge changed. Returns { ok, parent, at, undone, skipped }. Rows changed since fail it (409) unless skipChanged.

Reading the past​

MethodArgumentsReturns
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 atrows 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)
ArgumentDefault
worldshow many, more than 0
prefixworld i is named <prefix>_<i>
scriptSQL run in each world; $1 is its index, $2 its seed
scorea SELECT giving one number per world
keepallworlds to keep, best first, or "all"
orderdescdesc: highest score best; asc: lowest
seed0seeds random() in every world
threadsevery 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:

FieldTypeScriptPython
messagee.messagestr(e)the server's message, which says what to fix
statuse.statuse.status400, 401, 403, 404, 409, 413, 429, 500 (see HTTP API)
conflictse.conflictse.conflictsa merge's conflicting rows; [] otherwise
  • No SQLSTATE: the server's sql errors carry code (e.g. 42P01), but ChronosError keeps only status, message and conflicts. Use a Postgres driver when you need the code.
  • Not ChronosError: a network error (TypeError from fetch, 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.