Skip to main content

MCP

chronos mcp <folder> [--allow-merge | --agent NAME] [-b branch]

Serves a database folder to an AI agent as MCP tools, over stdio; chronos serve serves the same tools over HTTP, to an agent with its token (below). Why worlds work the way they do: concepts. Who may do what: security.

claude mcp add chronos -- chronos mcp /path/to/mydb

Modes​

CommandActs asTools listedChanges
chronos mcp mydbsafe mode (the default)27: all but merge, restore, undo_mergeonly worlds forked in this session
chronos mcp mydb --agent botthe agent botthose bot has the rights for (below)what bot may change; its forks stay its own across sessions
chronos mcp mydb --allow-mergethe database's own userall 30everything
  • --agent wins: with both --agent and --allow-merge, the session acts as the agent.
  • --agent takes a name, not a token: an unknown or disabled agent is refused at start (chronos: ..., exit 1). Anyone who can run chronos mcp on the folder can name any agent; see security.
  • -b branch: the branch must exist or the command exits 1. Tools don't use it: each call names its own branch (default main).
  • The folder already open: if chronos serve, the shell or another chronos mcp has the folder open, chronos mcp joins that process over its Unix socket and gets the same modes. --token, --safe and --admin-token don't apply to it.

Adding it to a client​

Claude Code:

claude mcp add chronos -- chronos mcp /path/to/mydb
claude mcp add chronos -- chronos mcp /path/to/mydb --agent bot

Any client that starts stdio servers from a JSON config (mcpServers):

{
"mcpServers": {
"chronos": { "command": "chronos", "args": ["mcp", "/path/to/mydb"] }
}
}

Use an absolute folder path: the client picks the working directory.

Over HTTP (chronos serve)​

chronos serve answers MCP at POST /mcp, as the agent whose token the client sends (Authorization: Bearer <token>), so the client needs only that token: not the folder, not the chronos binary. This is how an agent in a container or on another machine connects (see agents in containers).

claude mcp add --transport http chronos https://db.internal:7070/mcp --header "Authorization: Bearer $BOT_TOKEN"
  • Who it acts as: always an agent, the same as chronos mcp --agent with that agent's name: its rights decide which tools are listed and what they may change, its quotas apply (max_concurrent counts each tool call, and each statement of a sql call; the SQL any tool runs has its max_query_ms and max_memory_mb), and what it does is in the audit trail. describe shows no rows of a table a row-level security policy limits it on. Under --safe, a request with no agent's token is the agent guest: one with no token on a server without --token, or with the server's token. Every such client is the same guest, sharing its worlds (any of them can change or discard what another guest forked) and its rights, and the guest has no quotas: give each agent a token of its own. The server's token, the admin token, and no token on a server without --safe get 403: the database's own user uses chronos mcp <folder> on the database's machine. A wrong, disabled or dropped agent's token gets 401, and wrong tokens back off per address as on the rest of the API.
  • Transport: MCP's Streamable HTTP, without its event stream. Each request's body is one JSON-RPC message, and a request's reply is the response's JSON body (200). A notification, or a client's response, gets 202 with no body. A batch (a JSON array) gets 400 (-32600), and a body that isn't JSON gets 400 (-32700). GET /mcp (an event stream) gets 405. There are no session IDs: nothing carries over between requests, as nothing does between stdio's calls.
  • Sizes: a message may be at most 8 MB (413 from its Content-Length, before the body is read). Over HTTP and stdio alike, find and history give at most 1000 rows a call, find pages to offset 100000 with a beam (ef) up to 4096, and past those the call says to read with the sql tool.
  • The rest of the API's rules apply: loopback only unless the server has a token and TLS; a request from a web page (with an Origin header) gets 403; the body must be sent with Content-Type: application/json.

Protocol​

  • Transport: stdio, JSON-RPC 2.0, one message per line. stdout carries only protocol messages; blank lines are skipped. Over HTTP, see above.
  • Protocol version: the server answers initialize with the client's protocolVersion, or 2025-06-18 if it sends none.
  • Capabilities: tools only. No resources, no prompts.
  • Methods: initialize, ping (replies {}), tools/list, tools/call. A message without an id (a notification such as notifications/initialized) gets no reply.
  • Server info: {"name": "chronos", "version": "<chronos version>"}, plus instructions that teach the workflow: describe, fork, change your branch, diff, then merge or discard. The instructions follow what the session may do. If it may not merge (safe mode, or an --agent without merge or merge_own), they say a person merges with chronos <database folder> merge <branch>, and to report the branch name. If it may merge its own branches (--allow-merge, or an --agent with merge_own, merge or admin), they tell it to call merge when its diff is right, and that a merge policy may hold the merge for a person: then it stops and reports the branch and the reasons.
  • State: none between calls, except the worlds a safe-mode session forked. Each sql call is its own session: a transaction must BEGIN and COMMIT within one call.

Errors​

A tool that fails replies with a normal result, isError: true, and a message saying how to fix the call:

{"jsonrpc":"2.0","id":8,"result":{"content":[{"type":"text","text":"'id' is required: a non-empty string without spaces"}],"isError":true}}

Refusals (safe mode, an agent's rights), unknown tools (unknown tool 'x'; see tools/list), SQL errors and merge conflicts all come back this way. Protocol errors use JSON-RPC error:

CodeWhen
-32700the line isn't JSON (id is null)
-32601unknown method, resources/list and prompts/list included
  • Names: table, id, name, branch (where required) and world must be non-empty strings without spaces.
  • Results are text: the shell's output for the same command, SQL results as a psql-style table ( name\n--------\n Ada L.\n(1 row)).

Tools​

Every tool that reads or writes rows takes branch (string, default main). A branch as it was, name@when, works wherever a branch is read (see worlds).

ToolDoesSafe mode
describetables, fields, a sample rowyes
findfilter, text and vector searchyes
sqlrun SQLreads; writes on its own worlds
getread one rowyes
putcreate or replace one rowon its own worlds
deletedelete one rowon its own worlds
forkcreate a branchyes
set_metaset a branch's noteson its own worlds
brancheslist branchesyes
diffwhat changedyes
historyrecent events of a branchyes
restoreput a branch back to a momentnot listed
mergeapply a branch to its parentnot listed
merge_previewwhat a merge would doyes
rebasetake in what changed in the parent, keep the branch's own changeson its own worlds
check_readswhether anything the branch read changed since it forkedyes
undo_mergeundo a branch's latest mergenot listed
checkpointname a moment of a branchon its own worlds
rollbackgo back to a checkpointon its own worlds
discardthrow a branch awayon its own worlds
simulatefork, run and score many worldsyes; kept worlds are its own
replayre-run a simulated worldyes
merge_policiesthe rules agents' merges keep to, and who keeps to eachyes
check_merge_policytry rules on a branch's real changesyes
set_merge_policycreate or change a policy, give it to agentsdrafts the SQL for a person
drop_merge_policyremove a policydrafts the SQL for a person
reviewsmerges waiting for a personyes
reviewa policy's reviewer decides a held merge: approve, reject or passyes
merge_checksqueries every merge's result must find nothing inyes
set_merge_checkcreate or replace a merge checkdrafts the SQL for a person
drop_merge_checkremove a merge checkdrafts the SQL for a person

describe​

Tables with row counts, their fields with types and example or allowed values, and a sample row. Call first.

ParameterTypeRequiredDefault
tablestringnoall tables
branchstringnomain

find​

Exact filters, plus optional text search (BM25) and/or vector similarity, best first. Returns the match count (1 match, 3 matches), then up to limit rows with scores. With neither text nor vector, lists matching rows in key order. See search.

ParameterTypeRequiredDefault
tablestringyes
whereobjectnonone; {"field": value, ...}, all must match
textstringnowords to find in the rows' string fields
vectorarray of numberswith vector_fieldranks by dot product
vector_fieldstringwith vectorthe field holding stored vectors
limitintegerno20
offsetintegerno0
efintegerno0: chosen automatically; the vector graph's beam
branchstringnomain

vector without vector_field (or the reverse) fails: give both 'vector' and 'vector_field' ....

sql​

Runs SQL on a branch, as its own session. $1, $2, ... take params. See SQL and worlds.

ParameterTypeRequiredDefault
sqlstringyesone or more statements; BEGIN ... COMMIT within the call
paramsarraynonone; JSON values for $1, $2, ...
branchstringnomain

Returns each result as a table, or a statement's tag.

get​

Reads one row.

ParameterTypeRequiredDefault
tablestringyes
idstringyes
branchstringnomain

put​

Creates or replaces one row (the whole row).

ParameterTypeRequiredDefault
tablestringyes
idstringyes
recordobjectyesthe row as a JSON object
branchstringnomain

delete​

Deletes one row.

ParameterTypeRequiredDefault
tablestringyes
idstringyes
branchstringnomain

fork​

Creates a branch: an instant private copy. In safe mode it becomes one of the session's own worlds.

ParameterTypeRequiredDefault
namestringyese.g. your task id
fromstringnomain; a branch, or name@when
metaobjectnonone; notes, e.g. {"owner": "you", "task": "..."}

To say what the branch is meant to change, held to at its merge, put a declared scope in meta: {"may_change": "leads.status, lists", "tenant": "org_id = 42", "max_rows": 250, "max_deletes": 0, "intent": "close stale leads", "run": "your run or subagent id"}. To say which run made its changes (shown in the audit and in reviews, and undone together with UNDO AGENT ... WHERE run_id), add run_id, prompt_version, model and tool_schema (which run made it).

Returns created branch task-1 (world id <id>) from main; pass branch="task-1" to work on it.

set_meta​

Merges a JSON object into a branch's metadata; a null value removes a key. Returns <branch>: <metadata>.

ParameterTypeRequiredDefault
branchstringyes
metaobjectyes

branches​

Lists branches with their parent and number of changes. No parameters.

diff​

What changed on a branch since it was forked, row by row and field by field: the count first, then a page of changes.

ParameterTypeRequiredDefault
branchstringyes
tostringnonone: compare with the fork point. Else from branch to to; either can be name@when
as_sqlbooleannofalse; true: the change as SQL statements (INSERT, UPDATE, DELETE, CREATE/ALTER TABLE)
readersbooleannofalse; true: instead of rows, what in the database reads each changed column, then what that couldn't see (DIFF ... READERS)
limitintegerno20
offsetintegerno0

Without to: 2 changes on task-1 (leads: 1 deleted, 1 updated, 0 inserted), what each table loses first, then one line per change, the deleted rows first with what they held: - DELETE table/id {row}, then + table/id {row} or ~ table/id field: old -> new. With count: true, the same per-table line alone, and what the branch's merge would send (; effects on merge: email (2)). With to, the table from DIFF ... TO. A longer diff ends with (showing changes 1-20 of 57; call diff again with offset=20 for the next ones).

history​

What happened to a branch recently, newest first: forks, writes, commits, merges. Runs SHOW HISTORY.

ParameterTypeRequiredDefault
branchstringyes
limitintegerno30

restore​

Puts a branch back as it was at a moment, as an ordinary write. Runs RESTORE WORLD.

ParameterTypeRequiredDefault
branchstringyes
atstringyesa time (2026-09-20 10:00) or a time ago (-10 minutes)

merge​

Applies a branch's changes to its parent and deletes the branch. If rows both sides changed, nothing is merged and the conflicts come back (isError: true), with a hint naming resolve, by_columns, rows and merge_preview. With only_tables, only_keys or into, just those rows merge (or go to another branch) and the branch stays. See MERGE.

ParameterTypeRequiredDefault
branchstringyes
intostringnothe parent; another branch to merge into (the branch stays)
only_tablesarray of stringsnoall tables; non-empty (the branch stays with the rest)
only_keysarray of stringsnoall rows; "table/id", non-empty (the branch stays with the rest)
by_columnsbooleannofalse; true: only a column both sides changed conflicts
rowsobjectnonone; {"table/id": "ours" | "theirs" | "delete" | {row to keep}}
resolvestringnofail; ours (this branch's values) or theirs (the parent's)
confirmbooleannofalse; after a crash, merge the flagged branch as it is now (check its diff first)
versionintegernonone; merge only if the branch is still at this version (what merge_preview or reviews showed), refused otherwise
targetintegernonone; with rows settled (resolve, by_columns, rows), the target merge_preview named: refused if the parent changed since
confirm_deletesintegernonone; for a person approving a merge whose merge policy has confirm_deletes_over: the number of rows it deletes (merge_preview gives it). A wrong count is refused with the real one; an agent's own merge goes to review instead

Returns merged 1 change from task-1 into main. confirm applies to a plain merge only: it's ignored when into, only_tables, only_keys, by_columns, rows or version is given.

rebase​

Moves a branch's fork point up to its parent as it is now, keeping the branch's own changes on top, without changing the parent: a long-running branch picks up what changed under it and settles conflicts as they come. Refused, with nothing changed, on a conflict nothing settles, or when a row the branch read (with check_reads) changed since. Runs REBASE WORLD.

ParameterTypeRequiredDefault
branchstringyes
by_columnsbooleannofalse; true: only a column both sides changed conflicts
rowsobjectnonone; {"table/id": "ours" | "theirs" | "delete" | {row to keep}}
resolvestringnofail; ours or theirs
versionintegernonone; rebase only if the branch is still at this version

Returns REBASE n, n the parent's rows it took in.

check_reads​

Whether anything a branch read changed since it forked, without merging or changing anything: for a branch forked with (check_reads = true) (or by an agent whose merge policy has check_reads). The rows, key ranges and filtered scans it read, an empty result included, compared with its parent as it is now, and in a folder attached to Postgres with Postgres as it is now too. Meant for the start of each step of a long task: if something changed, read it again and decide again (rebase first) before writing; the branch's merge would be held for it anyway. The branch's view doesn't change. Runs CHECK READS.

ParameterTypeRequiredDefault
branchstringyes

Returns "Nothing ... read changed since it forked" or a table: checked_against (the parent, or Postgres (...)) and changed (what, and why).

merge_preview​

What merge would do with the same parameters, without doing it: per row, whether it applies or conflicts and why, with the fork point's, the branch's and the parent's versions. Runs MERGE ... DRY RUN; its columns are table, id, outcome, detail, base, ours, theirs, result, postgres. It leads with a row per table (outcome summary: 150 deleted, 1 updated, 1 inserted), then the rows it deletes (DELETE: the merge deletes this row), then the rest. In a folder attached to Postgres, a merge into main is checked against Postgres as it is now too (read only, rolled back): postgres is each row as Postgres has it, a row changed there since the copy has outcome changed in postgres, and a row with outcome postgres says when it was read. Then What reads the changed columns: and the table from DIFF ... READERS for the branch, and the branch's version: pass it as version with later pages (offset), which are refused if the branch changed since, and with merge, which then merges exactly what the pages showed.

ParameterTypeRequiredDefault
branchstringyes
into, only_tables, only_keys, by_columns, resolve, rowsnoas for merge
limitintegerno20
offsetintegerno0
versionintegernonone; with a later page, the version the first page named (refused if the branch changed since)

undo_merge​

Puts back, in the parent, every row the branch's latest merge changed. Refuses if some were changed since, naming them. Runs UNDO MERGE.

ParameterTypeRequiredDefault
branchstringyesthe merged branch (it's gone; its name still works)
skip_changedbooleannofalse; true leaves rows changed since the merge alone

checkpoint​

Names this moment of a branch. Runs CHECKPOINT WORLD.

ParameterTypeRequiredDefault
branchstringyes
namestringyese.g. before-migration

rollback​

Puts a branch back as it was at one of its checkpoints, as an ordinary write (its history keeps the rest).

ParameterTypeRequiredDefault
branchstringyes
checkpointstringyesa name given to checkpoint

discard​

Throws a branch and its changes away.

ParameterTypeRequiredDefault
branchstringyes
cascadebooleannofalse; true also discards every branch forked from it, and from those

simulate​

Forks worlds branches from one moment of from, runs script in each in parallel ($1 is the world's index 0, 1, ..., $2 its seed), scores each with score, keeps the best keep and discards the rest. random() is seeded per world, so the same call gives the same worlds. Kept worlds are the session's own. See SIMULATE.

ParameterTypeRequiredDefault
worldsintegeryesmore than 0
prefixstringyesworld i is named <prefix>_<i>
scriptstringyesSQL run in each world
scorestringyesa SELECT giving one number per world
fromstringnomain; or name@when
keepinteger or "all"noall
orderstringnodesc (highest score best); or asc
seedintegerno0
threadsintegerno0: all free cores

Returns <n> worlds from <base> in <ms> ms, best first; <f> failed; kept: <names>, then up to 20 lines <name> (index <i>): score <s>[, kept] (or failed: <why>), then (<n> more).

replay​

Makes a simulated world again from its recorded inputs and says whether it comes out identical, rows and score.

ParameterTypeRequiredDefault
worldstringyesa world a simulation kept
asstringnonone; keep the replay under this name

Returns <world>: identical (0 rows differ; score <s> now, <s> recorded), with ; failed: <why> and ; kept as <name> when they apply.

merge_policies​

The merge policies: the rules under which an agent's merge goes through without a person. A psql-style table (name, max_rows, max_deletes, tables, review_tables, schema, overwrite, created), then a line per agent keeping to one (agent bot keeps to small), or no agent keeps to a merge policy. No parameters. Runs SHOW MERGE POLICIES.

check_merge_policy​

Tries rules on a branch's real changes before any agent keeps to them: would its merge go through on its own, or wait for a person, and why. Give a policy's name, rules, or both: the rules change the named policy's for this check only, so "what if max_rows were 1000?" is one call. Nothing is saved.

ParameterTypeRequiredDefault
branchstringyes
namestringnonone: only rules (and the defaults for the rest)
rulesobjectnonone; as for set_merge_policy

Returns draft's merge would go through on its own under those rules, or draft's merge would wait for a person under merge policy small: it changes 3 rows (at most 2) (each broken rule, joined with ; it ). Rows both sides changed aren't a rule: see them with merge_preview.

set_merge_policy​

Creates a merge policy, or changes the rules given of an existing one (the others stay), and has agents keep to it. Runs CREATE/ALTER MERGE POLICY and ALTER AGENT ... SET (policy = ...) as one call, so an agent needs admin (it isn't listed otherwise). In safe mode nothing changes: the reply (isError: true) is the SQL for a person to run, so an agent can draft rules and a person applies them.

ParameterTypeRequiredDefault
namestringyesletters, digits, _ and -
rulesobjectnonone (a new policy gets every default)
agentsarray of stringsnonone; agents to keep to it from now on
RuleTypeA merge needs a person whenDefault
max_rowsinteger or nullit changes more rows than thisnull: no limit
max_deletesinteger or nullit deletes more rows than this; 0: any delete (the recommended start for any agent)null: no limit
rows_per_hourinteger or nullwith the agent's other merges into main in the last hour, it changes more rows than thisnull: no limit
deletes_per_hourinteger or nullwith the agent's other merges into main in the last hour, it deletes more rows than thisnull: no limit
max_agestring, integer or nullits branch was forked longer ago than this ("1h", or ms)null: no limit
tablesarray of stringsit changes a table not in the list[]: any table
review_tablesarray of stringsit changes any table in the list[]
review_columnsarray of stringsit changes any column in the list ("users.email"), even in one row[]
schemabooleanfalse and it changes a table itself, a view, function, sequence, schema or typefalse
overwritebooleanfalse and it overwrites rows its parent changed since the forkfalse
criticalbooleanfalse and it changes a column a reader marked critical reads (MARK READER)false
check_readsbooleantrue: its agents' worlds keep what they read, and a merge that read rows changed since the fork waits (stale reads)false
require_scopebooleantrue: its agents' forks must declare may_change or tenant in meta (declared scope)false

An unknown rule, or a value of the wrong type, is refused with the rules' names. Returns the policy as a table.

{"name": "small", "rules": {"max_rows": 500, "max_deletes": 0, "review_tables": ["payments"]}, "agents": ["bot"]}

drop_merge_policy​

Removes a merge policy. Refused while an agent keeps to it, unless release_agents (DROP MERGE POLICY ... CASCADE: released and dropped in one step). Needs admin; in safe mode, drafts the SQL like set_merge_policy.

ParameterTypeRequiredDefault
namestringyes
release_agentsbooleannofalse; true: take the policy from its agents in the same step (their rights alone decide then)

reviews​

Merges a policy sent to a person, oldest first: world, owner, policy, reasons, asked, version, changed_since (the branch was written to, or partly merged, after its agent asked), scope, changes (per table, deleted, updated and inserted, deletes first), effects (what its merge would send, per channel). A person approves with merge (or MERGE WORLD), or throws it away with discard. No parameters. Runs SHOW REVIEWS.

review​

For a merge policy's reviewer (the policy's reviewer names the agent): decide a held merge before a person does. approve merges it as it was held (refused if the branch changed since), reject keeps it from merging, pass leaves it to a person; the reason is kept in reviews and the audit. Runs REVIEW WORLD ... APPROVE | REJECT | PASS REASON '...' (a reviewer before a person).

ParameterTypeRequiredDefault
branchstringyes
verdictapprove, reject or passyes
reasonstringyes

merge_checks​

The merge checks: SQL queries every merge's result must find nothing in. A merge whose result one finds rows in is refused with the rows, or for an agent keeping to a merge policy waits in reviews. A psql-style table (name, tables, timeout_ms, query, created). No parameters. Runs SHOW MERGE CHECKS.

set_merge_check​

Creates a merge check, or replaces the one of that name (CREATE OR REPLACE MERGE CHECK). Needs admin (it isn't listed otherwise); in safe mode nothing changes and the reply (isError: true) is the SQL for a person to run.

ParameterTypeRequiredDefault
namestringyesletters, digits, _ and -
querystringyesone SELECT that changes nothing; the rows it finds are what's wrong
tablesarray of stringsnonone: every merge runs it. Names are taken as written ("Lists" stays capitalized; "app.orders" is table orders in schema app)
timeout_msintegerno1000; a merge it doesn't finish in is refused
{"name": "lists_keep_leads", "query": "select id from lists where id not in (select list from leads)", "tables": ["leads", "lists"]}

drop_merge_check​

Removes a merge check. Needs admin; in safe mode, drafts the SQL like set_merge_check.

ParameterTypeRequiredDefault
namestringyes

Safe mode​

The default, without --allow-merge or --agent. The agent reads every world and forks; a person approves.

  • Not listed, and refused if called: merge, restore, undo_merge (safe mode: agents can't merge. A person reviews your diff and runs ...).
  • Writes need one of its own worlds: put, delete, set_meta, discard, checkpoint, rollback, and sql that writes (BEGIN/COMMIT, EXPLAIN ANALYZE of a write, and a SELECT calling a function that writes included) need branch set to a world forked in this session (by fork, or kept by simulate or replay). No branch: safe mode: changes need branch="<a branch you forked>". Call fork first. main or another world: ... and 'main' isn't one.
  • discard with cascade: refused if any world forked from it wasn't forked in this session.
  • World statements in sql are refused on any branch: forking, merging, restoring, dropping or switching worlds, and changing system settings (safe mode: agents can't fork, merge, restore, drop or switch worlds in SQL, ...). Reads such as SHOW WORLDS, DIFF, MERGE ... DRY RUN and AS OF work. Use the fork tool to fork.
  • Session-scoped: "its own worlds" lasts as long as the process. A new chronos mcp can't change worlds an earlier one forked; use --agent for forks that stay the agent's.
  • Merging: a person runs chronos <folder> merge <branch> in a terminal, or MERGE WORLD from psql.
  • Merge policies: merge_policies, check_merge_policy and reviews work. set_merge_policy and drop_merge_policy change nothing: they reply with the SQL for a person to run, so the agent drafts rules (tried with check_merge_policy first) and the person applies them. merge_checks works; set_merge_check and drop_merge_check reply with the SQL the same way.
  • Not the same as chronos serve --safe: that makes HTTP and Postgres clients without an agent's token act as the agent guest (see HTTP API). It doesn't change chronos mcp.

As an agent​

With --agent NAME, each call is checked against that agent's rights and quotas (see concepts), and what it writes is in the audit trail. Tools it can't use aren't listed:

ToolListed when the agent has
mergemerge or merge_own
restore, rollbackrestore
undo_mergeadmin, or restore and merge; never with a merge policy
set_merge_policy, drop_merge_policy, set_merge_check, drop_merge_checkadmin
  • Checked per call: put and delete as writes to branch; merge as a merge of branch (or into into); restore; set_meta and discard as managing that world (cascade: its whole tree); sql, checkpoint, rollback and undo_merge statement by statement, as the Postgres port checks them; fork, simulate and replay as they run; everything else as a read of branch.
  • Refused calls come back with isError: true and the reason.
  • An agent with a merge policy: its merge goes through when the merge keeps to the policy's rules. When it breaks one, it's refused (merging bot-2 needs a person's review (merge policy small: it changes 5 rows (at most 2)); it's queued (SHOW REVIEWS)) and the branch waits in reviews for a person. merge_preview shows a blocked row saying so first. See the guide. A merge check the merge breaks is one more broken rule: queued with the rows it found. An agent without a policy is refused (merging bot-2 breaks merge check keep, which finds (id = 1); nothing was merged).

Example session​

Safe mode, one JSON-RPC message per line (replies shortened to their text):

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"fork","arguments":{"name":"task-1","meta":{"task":"fix names"}}}}
created branch task-1 (world id ...) from main; pass branch="task-1" to work on it
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"put","arguments":{"table":"users","id":"1","record":{"name":"Ada L."},"branch":"task-1"}}}
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"diff","arguments":{"branch":"task-1"}}}
1 change on task-1
~ users/1 name: "Ada" -> "Ada L."
{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"merge_preview","arguments":{"branch":"task-1"}}}

The preview shows users/1 with outcome apply. The agent reports task-1, and a person merges:

chronos /path/to/mydb merge task-1