The problem
A plumber, a tenant and a building superintendent have to be in the same flat at the same time. Nobody shares a calendar. Two of the three do not read email. The job is one line to describe and an afternoon to do, because it is a phone problem: ring everybody, find the overlap, then ring everybody back.
The naive version of that over an API is worse than the human version. Ask the plumber "can you do Thursday at two" and he blocks Thursday afternoon. Then the tenant cannot make it and the plumber has held an afternoon for a job that is not happening and nobody ever tells him.
What it does
Two phases. The split is the whole idea.
Gather promises nothing. Every call says out loud that nothing is booked and that a second call will confirm one time once everyone has answered. The call collects which options work. The feasible set narrows after each answer, so the second person is asked about fewer options than the first. A run stops the moment the overlap is empty rather than finishing the roster out of politeness. On the demo request that turns eight worst case calls into two.
Confirm commits once. One time, put to everybody, only then. A person who says yes has agreed to keep an afternoon free. That is a duty whether or not the run succeeds.
Release pays the duty. If the commit fails after somebody said yes, everyone who said yes gets a call telling them it is off. That call is owed even when the process died, which is what the ledger is for.
The ledger is the interesting part
Every call, every answer and every phase boundary is appended to a JSONL file before the next call goes out. That file is not a log. It is the state.
replayrecomputes the feasible set and the outcome from the recorded answers alone. If the recomputed answer disagrees with the recorded one, the ledger is refused rather than trusted. Widening a feasible set, booking a slot the answers do not support and a confirmed outcome with no confirmation behind it are all caught.resumefinishes a coordination whose process died. It re-issues the exact idempotency key the ledger recorded rather than deriving a new one, because a derived key changes the moment any call script in the repository changes. A different key rings a second phone.- Running a finished coordination twice is reading it twice. Nothing is appended and nobody is called.
- The window belongs to the coordination rather than to the process. A yes recorded before a crash is judged against the window it was given in, so a resume an hour later cannot act on a promise that has expired.
What it refuses to do
- It will not confirm on an answer it could not place inside the window. It will not treat a party as unreached while a call that might have connected is still live.
- It will not settle a confirm call for a time that has already started.
- It will not re-issue a recorded key against a different payload, a different provider origin or a different account. All three are recorded beside the key, because a key only means anything inside one namespace.
- It will not write a confirmation code into the ledger file.
How it was built
TypeScript on @call-e/calle, tsx plus node:test, no build step, plus a local fake CALL-E so the whole two phase protocol runs with no account. The fake honours idempotency keys, which matters: a stub that replays by script identity cannot tell a retry from a replay. A retry test written against one proves nothing.
The design went through ten rounds of maintainer review before it merged. Most rounds were the same lesson in a new place. Do not trust a recorded field, re-derive it. That is why replay runs on the handback path and not only on demand.
What is real
158 tests, tsc --noEmit clean, npm run demo exit 0 across five cases including a deliberately tampered ledger that replay catches. One honest note for anybody running it today: on main three of those tests fail, because the test fixture pins its appointment slots to 2026-08-06 and that date has passed, so the app correctly refuses to confirm a slot that has already started. The fix is PR #382, which gives the fixture the clock it belongs to. On that branch the suite is 158 of 158.
Built With
- append-only-ledger
- call-e
- call-e-sdk
- crash-recovery
- idempotency
- json-schema
- node-test-runner
- node.js
- two-phase-commit
- typescript
Log in or sign up for Devpost to join the conversation.