Drift and conflicts
Two people edit the same page. ctx handles that without ever overwriting either side: it keeps a copy of the version you last synced, compares your file and the server’s version against it, and when both moved it records a conflict and waits for you to decide. Nothing is merged behind your back, and nothing is lost.
This guide covers the model (base, local, server and the nine page states), what ctx does
when it finds a conflict, and the ways out: by hand with ctx show and ctx resolve, through
an agent, or in VS Code.
Three versions of every page
Section titled “Three versions of every page”For every page it has synced, ctx keeps a base: the exact bytes of the version you last
pushed or pulled. It lives next to your knowledge, under .contexo/.sync/:
| Path | Holds |
|---|---|
.sync/state.json |
page_shas: per page, the server commit sha of its base |
.sync/bases.json |
per page, the base’s sha, the hash of its content and synced_at |
.sync/base/<path> |
the base bytes themselves |
.sync/conflicts.json and .sync/conflicts/<path> |
conflict sidecars (see below) |
.sync/backup/<path>.<UTC time> |
your previous file, saved by ctx resolve --theirs |
.sync/lock |
held while a push, pull, resolve or repair runs |
Editing a page never touches any of this. The base only moves when a page is pushed, pulled, resolved or repaired, which is exactly the point: it records the version you started from.
.contexo/.sync/ contains its own .gitignore with *, so these copies are never committed,
even by teams that commit .contexo/. ctx writes everything there itself; never edit it by
hand.
Each page then has three sides:
- base: the version you last synced;
- local: your working file under
.contexo/; - server: the version the server has now.
ctx compares local against base by content (normalized, so formatting-only rewrites do not count) and server against base by sha. The answer is one of nine states:
| State | Base, local, server | What to do |
|---|---|---|
unchanged |
local equals base, server still at the base | nothing |
modified |
you edited it, server still at the base | push |
new |
your page, never synced, not on the server | push |
incoming |
you did not edit it, the server moved | pull |
incoming_new |
on the server, not on disk, never synced | pull |
conflict (both_modified) |
you edited it and the server moved | resolve |
conflict (both_added) |
your new page, and the server already has the same path | resolve |
deleted_local |
synced before, the working file is gone | deletes are not synced; ctx resolve <path> --theirs restores it |
invalid |
the file does not parse, or its path does not match its slug and type | fix the file |
unverified |
ctx cannot confirm the base (missing, or for another sha) | ctx repair, or the next online status, pull or push |
Online, the server side comes from one listing of the repo’s pages per run. There is no
remote cache: offline (ctx status --offline), ctx knows the server side of a page only from
a conflict sidecar, so it can still show conflicts but not incoming pages.
Seeing it: ctx status
Section titled “Seeing it: ctx status”ctx status groups the pages that need an action, and prints the command for each group.
Here you (the user dev) edited two pages while a teammate, bob, pushed his own edits to the
same two pages, plus a new page:
contexo ● connected
── REMOTE
server https://contexo.example.com (from credentials)
repo acme-api
user dev <dev@example.com>
auth authenticated (legacy)
── KNOWLEDGE
pages 4 · 2 conflict · 1 incoming · 1 unchanged
last pull a4b2c1d3
captures none pending
── CONFLICTS
wiki/concepts/stripe-subscription.md both_modified
wiki/entities/billing-worker.md both_modified
keep yours ctx resolve <path> --ours
take theirs ctx resolve <path> --theirs
── TO PULL
wiki/concepts/webhook-retries.md new on server
run ctx pullCONFLICTS names each page and its kind; TO PULL lists what the server has that you do not.
TO PUSH, INVALID, UNVERIFIED and DELETED LOCALLY sections appear the same way when they
have pages. With --json the same facts come as the ctx.status.v1 document, with each
page’s state, its conflict_kind, and the base, local and remote sides it has.
How a conflict is recorded
Section titled “How a conflict is recorded”A conflict sidecar is an entry in .sync/conflicts.json with the server’s sha, the base
sha, the kind and where it was found (push, pull or repair), plus the server’s version of
the page (sanitized) under .sync/conflicts/<path>. Three operations write one:
- a push that the server refused with a 409 (
source: push); - a pull that skipped a page as a conflict (
source: pull); - a base repair that found an old agent’s sha bump over your edit (
source: repair; see upgrading below).
The sidecar is cleared by a successful push of the page, by a pull that writes or adopts it,
and by ctx resolve. While it exists, the page is a conflict even offline, and even when a
later listing on its own would make your file look like a plain edit, so it can never be
pushed over the teammate’s version by accident.
Recording a conflict never moves the page’s base or its recorded sha. Here is a push that lost
a race: bob’s push landed between your push’s listing and its send, so two pages got a 409 and
the third was created. ctx push --json writes the push document, then the error envelope, and
exits 6:
{
"schema": "ctx.push.v1",
"dry_run": false,
"message": "ctx push (3 pages)",
"server_url": "https://contexo.example.com",
"repo_id": "acme-api",
"new_head": "e5a3b9c807162534435261708f9e0d1c2b3a4958",
"pushed": [
{
"path": "wiki/entities/invoice-service.md",
"kind": "create",
"sha": "e5a3b9c807162534435261708f9e0d1c2b3a4958"
}
],
"conflicts": [
{
"path": "wiki/concepts/stripe-subscription.md",
"kind": "both_modified",
"base_sha": "d2f0e617a3b5c7d9e1f2031425364758697a8b9c",
"server_sha": "f0c47d2196385a7b4c3d2e1f0a9b8c7d6e5f4a3b"
},
{
"path": "wiki/entities/billing-worker.md",
"kind": "both_modified",
"base_sha": "c1d8a3f50e2b4c6d8f90a1b2c3d4e5f607182930",
"server_sha": "3b8e1a0947c2f6d85e90a1b2c3d4e5f60718293a"
}
],
"skipped": [],
"distill": {
"required": false,
"bypassed": false
},
"warnings": []
}
{"schema":"ctx.error.v1","exit_code":6,"error":{"code":"conflict","message":"push: 2 conflict(s) remain; other pages were pushed","advice":["ctx resolve wiki/concepts/stripe-subscription.md --ours","ctx resolve wiki/concepts/stripe-subscription.md --theirs","ctx resolve wiki/entities/billing-worker.md --ours","ctx resolve wiki/entities/billing-worker.md --theirs"],"retryable":false,"http_status":409,"details":{"paths":["wiki/concepts/stripe-subscription.md","wiki/entities/billing-worker.md"]}}}
(exit 6)Pull is safe
Section titled “Pull is safe”ctx pull never overwrites a page you edited:
- an
incomingpage is overwritten with the server version, and anincoming_newpage is created; - a
modifiedpage is skipped; you push it later; - a
conflictis skipped and gets a sidecar; - a conflict whose file already equals the server version is adopted: the server version becomes its base, nothing is rewritten;
invalid,deleted_localpages, pages whose paths differ only by letter case, and pages that changed on disk while the pull ran are skipped and left alone.
Skips are normal results: ctx pull exits 0. Here the JSON document is followed by the human
run of the same pull, with the resolve advice:
{
"schema": "ctx.pull.v1",
"server_url": "https://contexo.example.com",
"repo_id": "acme-api",
"head": "e5a3b9c807162534435261708f9e0d1c2b3a4958",
"written": [
{
"path": "wiki/concepts/webhook-retries.md",
"sha": "7d25c6bf018a3e4d5c6b7a8990f1e2d3c4b5a697",
"kind": "create"
}
],
"adopted": [],
"skipped": [
{
"path": "wiki/concepts/stripe-subscription.md",
"state": "conflict",
"reason": "conflict",
"server_sha": "f0c47d2196385a7b4c3d2e1f0a9b8c7d6e5f4a3b"
},
{
"path": "wiki/entities/billing-worker.md",
"state": "conflict",
"reason": "conflict",
"server_sha": "3b8e1a0947c2f6d85e90a1b2c3d4e5f60718293a"
}
],
"reindexed": true,
"warnings": []
}
$ ctx pull
Pulled 0 page(s), adopted 0; HEAD=e5a3b9c8
Skipped 2 page(s):
wiki/concepts/stripe-subscription.md: conflicts with the server version; run ctx resolve wiki/concepts/stripe-subscription.md --theirs|--ours
wiki/entities/billing-worker.md: conflicts with the server version; run ctx resolve wiki/entities/billing-worker.md --theirs|--oursResolving by hand
Section titled “Resolving by hand”No agent needed. Take one conflicting page at a time.
1. Find the conflicts. ctx status lists them under CONFLICTS (above).
2. Look at both sides. Your version is the working file. Print the other two with
ctx show:
ctx show wiki/entities/billing-worker.md --at base # what you both started fromctx show wiki/entities/billing-worker.md --at server # what the teammate pushedctx show writes the raw page bytes and nothing else, so you can redirect them to a file or
feed them to diff. --at base needs no network; --at server reads the server’s current
version, sanitized exactly as a pull would write it.
---
schema: ctx.page.v1
slug: billing-worker
type: entity
author: dev
agent: claude
created: 2026-07-21T09:30:00Z
updated: 2026-07-25T11:20:00Z
parent_sha: ""
tags:
- billing
---
## What it is
`worker` consumes Stripe webhook events from SQS and retries failed
invoices on the schedule in stripe-subscription.
## Rationale
Considered retrying inside the webhook handler; rejected because Stripe
expects a fast 2xx and redelivers slow webhooks.---
schema: ctx.page.v1
slug: billing-worker
type: entity
author: dev
agent: claude
created: 2026-07-21T09:30:00Z
updated: 2026-07-25T11:20:00Z
parent_sha: ""
tags:
- billing
---
## What it is
`worker` consumes Stripe webhook events from SQS and retries failed
invoices on the schedule in stripe-subscription. It runs as one ECS task per region.
## Rationale
Considered retrying inside the webhook handler; rejected because Stripe
expects a fast 2xx and redelivers slow webhooks.Bob added one sentence to ## What it is. Compare that with what you changed in your file.
3. Decide. Either keep yours or take theirs.
Keep yours. Merge the teammate’s change into your working file by hand if you want both,
then run ctx resolve with --ours. It keeps your file exactly as it is and
re-bases it onto the server’s current version, so the page becomes modified. Then push:
ctx resolve wiki/concepts/stripe-subscription.md --oursctx push✔ Kept your version of wiki/concepts/stripe-subscription.md on top of server f0c47d2; now run ctx pushAbout to push 1 page(s) to acme-api:
[EDIT] wiki/concepts/stripe-subscription.md (~ Concepts)
Pushed 1 page(s); HEAD=a4b2c1d3The push sends your version with the teammate’s version as its parent, so it supersedes theirs on purpose. That is why it matters what you merged in step 2.
Take theirs. --theirs replaces your working file with the server version. If your file
differed, it is saved first under .contexo/.sync/backup/, with a UTC timestamp:
ctx resolve wiki/entities/billing-worker.md --theirs✔ Took the server version of wiki/entities/billing-worker.md (3b8e1a0); your previous file is saved at ~/projects/acme-api/.contexo/.sync/backup/wiki/entities/billing-worker.md.20260728T091407ZThe page ends unchanged, and the backup keeps your old content in case you want parts of it
back.
Guarding against a moving server
Section titled “Guarding against a moving server”--ours re-bases onto whatever the server has at the moment you run it. If a teammate could
push again while you are reviewing, pass the server sha you reviewed:
ctx resolve wiki/concepts/stripe-subscription.md --ours --if-server-sha f0c47d2196385a7b4c3d2e1f0a9b8c7d6e5f4a3bctx then refuses with stale (exit 6 with --json) unless the server is still at exactly that
sha, so you never supersede a version you have not seen. The sha is compared in full; a
shortened sha counts as stale. The VS Code extension always passes the sha the user looked at.
Two more rules:
--oursapplies only to aconflict(or anunverifiedpage with a valid file). Any other state is refused withnot_resolvable(exit 9 with--json) and nothing is written.--theirsworks on any state the server has: it also restores adeleted_localpage, and on anunchangedpage it only drops a leftover sidecar.
Without --json, a failed ctx resolve prints a human message and exits 1.
When an agent is driving
Section titled “When an agent is driving”Agents get the same guarantees through the MCP tools, which run the same sync engine:
ctx_pushsends only changed pages. A page that is a conflict is not sent; the agent gets a<MERGE_REQUIRED origin=push>brief with the ancestor, its own version and the server’s version, plus the sections both sides changed.ctx_pullkeeps conflicting pages and returns the same brief withorigin=pull.- The agent writes the merged page with
ctx_write_page, then callsctx_pushwithresolved: ["<path>"]. That isctx resolve --oursguarded by the server sha the agent was shown, followed by a push. If the server moved again, the agent gets a fresh brief and nothing is overwritten.
To keep merges rare, the MCP server also warns before the edit. When an agent reads a page
through ctx://wiki/<slug> or ctx://raw/<session-id> and the server has moved past the
page’s recorded sha, the read is prefixed with a <DRIFT_NOTICE> listing the changed sections
and pointing the agent at ctx_pull. The check needs a recorded sha (pages never pushed or
pulled are skipped), is cached for 60 seconds per page, and degrades to no notice when the
server cannot be reached. CONTEXO_DRIFT_DISABLE=1 turns it off.
Agent directives has the exact text.
After upgrading ctx, restart your agents
Section titled “After upgrading ctx, restart your agents”Agents run ctx mcp as a long-lived process, and a process started before ctx update keeps
running the old code until the agent restarts. Old versions answered a 409 by recording the
teammate’s sha as the page’s base without merging anything, so the next push would overwrite
the teammate’s change.
ctx repairs that when it can. If bases.json still holds the older base for the page, ctx
keeps it, so the page shows as a conflict (or as incoming when you had not edited it), never as
a plain edit; when you had edited it, the repair also writes a sidecar.
When there is no older base record, that cannot work: on the first run after upgrading from a
version without base snapshots, or after bases.json was lost, ctx can only take the server
version as the base. A page an old agent bumped before the upgrade can then show up as
modified, and pushing it would overwrite the teammate’s change.
So, after every ctx update:
- restart every agent session (Claude Code, Cursor, Codex, VS Code), so their MCP servers run the new ctx. The VS Code extension reminds you when it sees a new ctx version;
- if an old agent was working in the project, check
modifiedpages withctx show <path> --at serverbefore you push them.
How to not be here
Section titled “How to not be here”- Pull at the start of a session. The
ctx_pulltool description tells agents to do exactly this. - Leave drift detection on. The notice is cheap, and it is the only warning that arrives before the edit.
- Push soon after you write. The window between writing and pushing is the window in which someone else’s push turns yours into a merge.
- Run
ctx statuswhen you come back to a project after a few days.
Environment switches
Section titled “Environment switches”| Variable | Effect |
|---|---|
CONTEXO_DRIFT_DISABLE=1 |
No <DRIFT_NOTICE> on MCP page reads. |
CONTEXO_DISTILL_DISABLE=1 |
Bypasses the distill guard for ctx push and ctx_push; the push document records it as distill.bypassed. The VS Code extension removes it from the environment it gives ctx. |
CONTEXO_CAPTURE_DISABLE=1 |
ctx capture turn no-ops, so no buffer is written for the session. |
CONTEXO_LOCK_TIMEOUT |
How long push, pull, resolve and repair wait for .sync/lock before failing with busy (exit 12). A Go duration (3s) or whole seconds; default 10 seconds. |
See also
Section titled “See also”ctx status: the nine states,--offlineand--jsonctx showandctx resolve: the by-hand toolsctx pullandctx push: what each sends, skips and recordsctx repair: rebuild unverified basesctx_pushand agent directives: the agent side- VS Code extension: Keep Mine and Take Server Version
- The daily flow: the push and pull loop that leads here
