accelerando.wiki ↗ app ↗ github

Atomic commits

A common workflow: draft an invoice, attach three line items, mark it sent. Done as four separate tool calls, that's four R2 writes, four commit-log entries, four lines in the audit log, and an intermediate state where total=0 was briefly real.

That's not how bills work.

The transaction primitive

GitStore.transaction(message, fn) is a primitive that defers the commit:

const { result, commit, records } = await store.transaction(
  "create Invoice with 3 lines",
  async (tx) => {
    const invoice = await tx.create(INVOICE_ENTITY, {
      tenantId: "acme",
      fields: { customer_id, total: 1850 },
    });
    const lines = [];
    for (const line of incomingLines) {
      lines.push(await tx.create(INVOICE_LINE_ITEM_ENTITY, {
        tenantId: "acme",
        fields: {
          invoice_id: invoice.id,
          description: line.description,
          qty: line.qty,
          unit_price: line.unit_price,
          line_total: round2(line.qty * line.unit_price),
        },
      }));
    }
    return { invoice, lines };
  },
);

tx.create() writes the record to the backend immediately so it isn't lost on power-down. It does NOT commit. After fn resolves, the transaction emits a single commit() call against the backend with all the paths the inner writes touched.

The audit log shows one entry, not four:

3a4f8b2  create Invoice with 3 lines
       Christopher Bender, just now

       Files changed:
         data/acme/Invoice/8f3b2e1a-...agi
         data/acme/InvoiceLineItem/c91d7e5f-...agi
         data/acme/InvoiceLineItem/ad9320de-...agi
         data/acme/InvoiceLineItem/64bbf4f3-...agi

The user-facing tool

POST /tools/create_invoice_with_lines
{
  "tenant_id": "acme",
  "customer_id": "cus_001",
  "lines": [
    { "description": "Consulting hours", "qty": 10, "unit_price": 150 },
    { "description": "Travel", "unit_price": 350 },
    { "description": "Discount", "unit_price": -100 }
  ],
  "notes": "Q3 services"
}

The Worker:

  1. Validates the lines array. Non-empty, every line has a description and unit_price. qty defaults to 1.
  2. Computes the invoice total as sum(qty × unit_price). The agent doesn't pass total. The model doesn't do arithmetic. The DSL declares total: float REQUIRED and the executor satisfies it.
  3. Runs the transaction.
  4. Upserts every produced record into the projection so the next query reflects them.
  5. Returns { invoice, lines, commit_oid } so the caller can confirm what was created.

Small models fill the array correctly more often than they fill four sequential tool calls correctly. Atomicity at the protocol level helps the agent get it right.

The failure mode

If fn throws, the transaction skips the commit. The records that were written to R2 still exist — R2 has no transaction primitive — but the audit log has no entry for them. Effectively, they're orphans: no one reads them, no projection upserts them, no GitHub commit references them. They're recoverable forensically but invisible operationally.

A future tightening: stage writes to a tenant-scoped scratch prefix during a transaction, atomic-rename to the real prefix on commit. R2's API doesn't have atomic rename, but a Durable Object could provide the lock. See roadmap.

Where this leads

The transaction primitive is the foundation for general-ledger correctness. The next entity to land here is JournalEntry with JournalLine children, where the transaction enforces that debits and credits sum to zero before the commit fires. If they don't, the transaction throws and the books stay balanced. That's accounting at the API layer.