Claude Drives the sqlite-utils 4.0 Release: The Real Cost and Collaboration Methodology of AI-Assisted Refactoring

Simon Willison used Claude and GPT-5.5 to ship sqlite-utils 4.0, revealing coding agent costs and collaboration patterns.
Simon Willison leveraged Claude Code and GPT-5.5 cross-model review to finalize sqlite-utils 4.0, catching a critical data-loss bug in SQLite transaction handling. The process took 37 prompts, 34 commits, and an estimated $149 in API costs — offering a real-world benchmark for agentic engineering workflows, human-AI division of labor, and cost-aware AI collaboration in production open-source maintenance.
One of the biggest headaches for open-source library maintainers is shipping a major version update — major versions mean Breaking Changes, and once a design flaw slips in, it can take a very long time to correct.
It's worth understanding the background of Semantic Versioning (SemVer) here. The SemVer specification (semver.org) defines version numbers in a three-part major.minor.patch structure: patch increments represent backward-compatible bug fixes, minor increments represent backward-compatible new features, and a major version increment is a serious commitment to users — it signals backward-incompatible changes to interfaces or behavior, requiring users to actively migrate their code. SemVer is more than a numbering scheme; it's a "social contract" between maintainers and users in the open-source ecosystem. Major package managers like npm, PyPI, and Cargo deeply integrate SemVer semantics, allowing users to express dependency constraints through symbols like ^4.0.0 (compatible range) or ~4.0.0 (patch range). When a major version increments, package managers automatically block automatic upgrades, requiring users to explicitly confirm their intent to migrate — this is the cascading cost of major version changes. Because of this, if a design flaw sneaks into a major version, fixing it means incrementing the major version yet again, imposing additional migration costs on the entire ecosystem. For a utility library like sqlite-utils with numerous downstream dependencies, "the final review before the 4.0 release" isn't a routine formality — it's a genuine quality gate.
Prominent developer and Datasette creator Simon Willison recently shared a highly representative case study: he used Anthropic's Claude model to complete the final polish of sqlite-utils 4.0 stable. This wasn't just a demonstration of "AI-assisted programming" — it revealed the capability boundaries and collaboration patterns of coding agents in real production environments.
A "Data Loss" Bug That Was Nearly Missed
Simon's starting point was a minimalist prompt. He opened Claude Code for web on his iPhone and typed this single sentence:
Final review before releasing 4.0 stable — this is really important, need to find any last-minute issues that would constitute breaking changes if fixed later.
Claude promptly generated an initial report pointing to 5 "release blockers," the most severe of which could cause data loss:
Table.delete_where() was executing DELETE statements without wrapping them in an atomic() transaction like Table.delete() did — instead calling bare self.db.execute(). This would leave the connection stuck in an in_transaction=True state, causing all subsequent atomic() calls to fall into the savepoint branch, never committing.
To understand the severity of this bug, you need to know about SQLite's transaction state machine mechanism. A SQLite connection is in one of three states at any given moment: UNLOCKED (no transaction), DEFERRED/IMMEDIATE/EXCLUSIVE (explicit transaction in progress), or a "pending implicit" state caused by Python's sqlite3 module's implicit management layer. SQLite manages write operations through implicit transactions: when a DML statement (INSERT, UPDATE, DELETE) executes without an explicit BEGIN, SQLite automatically opens a transaction and commits it immediately after the statement completes. However, Python's sqlite3 module, under the default isolation_level non-None mode, automatically issues a BEGIN when it detects a DML statement but does not auto-commit — this behavior stems from its peculiar implementation of the DB-API 2.0 specification, runs counter to intuition, and is one of the most misleading design decisions in the module's history. This implicit transaction hangs open until commit() is explicitly called or the connection is closed. Notably, this "state pollution" is contagious: once an operation accidentally triggers an implicit BEGIN without committing, all subsequent code that relies on the in_transaction property for state checks will make incorrect decisions, creating cascading failures. The atomic() context manager relies on checking in_transaction to decide whether to start a new transaction or create a SAVEPOINT (SQLite's mechanism for nested transactions, operating through SAVEPOINT name, RELEASE name, and ROLLBACK TO name — allowing checkpoints within an existing transaction, though RELEASE only releases the savepoint without triggering a physical commit of the outer transaction). Once a bare execute() causes the connection to unexpectedly enter transaction state, all subsequent atomic() calls misjudge the situation as "already in a transaction" and create savepoints instead — and a savepoint's commit (RELEASE) is not equivalent to the outer transaction's COMMIT, leaving written data dangling until it's automatically rolled back when the connection closes.
The reproduction results were alarming: insert three rows, call delete_where once, insert new data and create a new table, close the database and reopen it — the deletion hadn't taken effect, and the newly inserted rows and even the entire newly created table had vanished into thin air. Simon said bluntly: "This is a genuinely bad bug, and I'm glad I didn't ship it."
On a notable detail, he also calmly pointed out that even if this issue had shipped, it would only have been a bug fixable via a 4.0.1 patch, not a design flaw requiring a 5.0 upgrade. This kind of triage judgment about "change severity" is precisely the irreplaceable value a senior maintainer brings to AI collaboration.
37 Prompts, 34 Commits: A Refactoring Done from a Phone
The entire fix process involved 37 prompts, 34 commits, touched 30 files, with net code changes of +1,321 / -190 lines. Simon shared an interesting observation:
The harder the task, the more it frees you up to do other things, because the agent sometimes needs 10 to 15 minutes to work through a new task.
This "asynchronous" feel isn't coincidental. The working mode of Coding Agents is fundamentally different from traditional code completion tools: they don't just generate code snippets — they autonomously plan multi-step tasks, invoke file read/write tools, execute test commands, and iteratively correct based on output. After each step, the Agent appends tool call results to the context window before proceeding to the next round of reasoning. This "perceive-plan-act" loop is based on the ReAct (Reasoning + Acting) framework — proposed by a Google research team in 2022 — whose work cycle is: the model generates a "Thought" → decides on an "Action" (e.g., invoking a file reading tool) → observes the "Observation" returned by the tool → continues reasoning based on the result, with all content from each cycle appended to the context window to form a complete "reasoning trace."
It's worth noting that the key difference between ReAct and earlier "Chain-of-Thought" prompting techniques is that CoT only reasons internally within the model and cannot perceive the real state of the external environment, while ReAct anchors reasoning to the actual codebase, test results, and file system through tool calls, significantly reducing the impact of model "hallucinations" on engineering tasks. This is also the fundamental reason why Coding Agents can reliably handle real codebases rather than merely generating plausible-looking code snippets. Claude Code, GitHub Copilot Workspace, Cursor Agent, and similar tools are all built on this paradigm. This architecture means a complex task may require dozens of internal iterations, each waiting for model reasoning and tool execution — which is exactly why it "needs 10 to 15 minutes." For users, this is both waiting and liberation: humans no longer need to watch every line of code being generated, instead playing the roles of "task initiator" and "quality reviewer."
So while Claude was heads-down modifying code, Simon went off to attend a Fourth of July parade in Half Moon Bay, only occasionally pulling out his phone to check progress and issue the next instruction. This "asynchronous collaboration" work pattern may be quietly reshaping programmers' daily rhythms.
The Transaction Model: The Core Design of This Release
The most important change in version 4.0 revolves around transaction handling. The logic of the new model is clear enough: every write method in the library — insert(), upsert(), update(), delete(), etc. — runs inside its own transaction and commits before returning. In other words, the moment a method call returns, the change is already persisted to disk.
db = Database("data.db")
db.table("news").insert({"headline": "Dog wins award"})
# The new row is already saved — no need to call commit()
Users only need to think about transactions in two scenarios: first, when they want to bundle multiple write operations into an "all or nothing" unit using db.atomic(); second, when manually managing transactions via db.begin() — the library will never commit a transaction you opened yourself.
While reviewing documentation, Simon also discovered a boundary issue he hadn't previously considered: Python 3.12 introduced an important behavioral change to the sqlite3 module. The new sqlite3.connect(..., autocommit=True/False) parameter allows connections to run in explicit autocommit mode compliant with the DB-API 2.0 specification — the most important compliance fix for this module since the Python 2.x era. In older versions, isolation_level=None was the only way to achieve "commit immediately after each statement," but this parameter name itself was misleading (named "isolation level" but actually functioning as a transaction control switch). The new autocommit parameter introduces a third compatibility state, sqlite3.LEGACY_TRANSACTION_CONTROL, for backward compatibility, but for libraries that rely on the connection.in_transaction property, all three modes' behavioral differences must be explicitly handled. The deeper background to this change is that the Python core development team, after years of discussion (related PEPs trace back to 2016), finally introduced the new API in 3.12 through PEP 249 compliance fixes while keeping old behavior as the default to avoid large-scale ecosystem disruption — this "progressive repayment of technical debt" strategy is quite common in major language runtimes. In autocommit mode, the isolation_level parameter is ignored, and the semantics of connection.commit() and connection.rollback() also change — this creates subtle but critical differences from the "implicit transaction" behavior of sqlite3 in Python 3.11 and earlier, which can cause code relying on old behavior to exhibit incorrect transaction commit timing in 3.12+ environments, leading to silent data loss. In testing, nearly the entire test suite would fail. He collaborated with the model to ensure the library wouldn't silently discard writes in this scenario. He also took away a useful lesson: reviewing documentation changes first is an excellent entry point for understanding code changes.
Having GPT-5.5 Review Claude's Work: Cross-Model Verification
The most insightful section of this story is Simon's shift in attitude toward "cross-model review." He admitted:
I used to think having one model review another model's work was kind of ridiculous, like some kind of superstition. The thing is — it actually works.
There's solid cognitive science and statistical reasoning behind this intuition. Different large language models have structural differences: they differ in pretraining data distribution, preference alignment strategies during the RLHF (Reinforcement Learning from Human Feedback) phase, and model architecture parameters. These differences cause different models to develop different "attention blind spots" when processing the same code — edge cases that one model tends to overlook due to its training data distribution may be exactly the scenarios heavily covered in another model's training.
This phenomenon has corresponding empirical support in the model evaluation field: researchers have found that error patterns across different model families on standard benchmarks exhibit significant "non-overlap" — the set of questions model A gets wrong but model B gets right, and vice versa, have relatively small intersections. This is direct evidence of "structural differences leading to complementary blind spots." From a probability standpoint, if model A's probability of missing a bug is p₁ and model B's is p₂, and the two are independent, the probability of both missing it drops to p₁×p₂, far lower than either alone — a principle aligned with "N-Version Programming" in software engineering, and the engineering practice that "multi-person code review" outperforms "self-review": independent perspectives bring cognitive diversity, and cognitive diversity is the key to finding blind spots.
He's now made it a habit to have Anthropic's strongest model review OpenAI's output, and vice versa. This time, he used Codex Desktop with GPT-5.5 xhigh for the review, with an equally concise prompt: "Review the changes since the last RC and confirm the changelog is up to date."
GPT-5.5 ultimately caught two P1-level issues that Claude had missed:
- Side effects of
db.query(): It callsdb.execute()first (triggering auto-commit) and only then checks whether the statement returns rows. This meansdb.query("update ...")throws aValueError, but the update has actually already been committed. - Commit timing for
INSERT ... RETURNING: The commit action is buried at the end of the returned generator, only triggered when the iterator is fully consumed. The commonnext(db.query(...))usage pattern leaves the transaction dangling, contradicting the documentation's promises.
Both issues were handed off by Simon to a fresh Claude session for verification, where they were all confirmed and fixed. He added that reviewing this code actually helped him "build a better mental model of SQLite transaction edge-case semantics" — AI collaboration here became a form of bidirectional learning.
The $149 Bill: A Real-World Reference for Agent Costs
Simon upgraded from the $100/month plan to the Claude Max $200/month tier. Out of curiosity, he wanted to know what this work would have actually cost at direct API pricing. He ran the AgentsView tool in an existing session to estimate, with these results:
| Session | Model | Cost |
|---|---|---|
| Main session | claude-fable-5 | $141.02 |
| API surface scan agent | claude-fable-5 | $2.40 |
| Transaction review agent | claude-fable-5 | $2.39 |
| Post-RC1 commit review agent | claude-fable-5 | $1.72 |
| Migration review agent | claude-fable-5 | $1.40 |
| Prompt counting agent | claude-opus-4-8 | $0.32 |
| Total | $149.25 |
The cost structure, with the main session accounting for $141 alone, is quite telling. Coding Agent token consumption is fundamentally different from normal conversation and exhibits a significant "snowball effect": after each action, the Agent appends the complete output of tool calls (file contents, test results, error messages, etc.) to the context, and as the task progresses, the context window continuously expands. For a medium-scale refactoring involving 30 files and 34 commits, the Agent may have cumulatively processed millions of input tokens throughout the process — and flagship models (like claude-fable-5) are priced at roughly 5-15x the input token rate of lightweight models like Haiku.
This cost structure reveals the economic difference between Coding Agents and "pay-per-call API usage": the cost of a single API call is negligible, but Agents continuously write large amounts of intermediate state into the context through dozens of tool call rounds, causing total token consumption to grow superlinearly. This also explains why "task complexity" and "cost" don't have a simple linear relationship — a task requiring frequent reads of large files may cost far more than a logically complex but file-size-small task. Common cost optimization strategies in the industry include: task decomposition (routing well-defined subtasks to lightweight models), context compression (periodically summarizing historical rounds), and "Prompt Caching" (charging discounted rates for repeated long-prefix tokens). This is precisely why Simon reflected afterward that he "should have delegated more subtasks to cheaper models": routing well-scoped subtasks (like "scan API surfaces" or "review migration guides") to Haiku or Sonnet-tier models can compress costs to less than one-tenth without significantly degrading quality.
The nearly $150 estimated cost made Simon glad he'd subscribed to the Max plan. This bill provides a valuable reference for the industry: using the strongest model to drive a medium-scale refactoring end-to-end is not cheap.
Even the Release Notes Were Delegated to AI
Also worth a separate mention is how the release notes were generated. Simon used to insist on hand-writing changelogs, but this time he had Claude write entries into the "Unreleased" section as each change landed, writing and reviewing in tandem. This created an elegant side effect: the changelog's commit history itself became a concise change summary.
Honestly, these release notes are better than what I would have written myself. Release notes are the kind of writing I'm happy to outsource to an agent — because they need to be boring, predictable, and accurate.
From exhaustive listing of breaking changes to clear descriptions of edge cases, the 4.0rc2 changelog covered improvements including db.execute() auto-commit, upsert() primary key validation, and WAL mode transaction protection. This kind of tedious yet critical documentation work is precisely where AI can most lighten the human burden.
A Mature Paradigm for AI Collaboration: Clear Boundaries in Human-Machine Division of Labor
What Simon Willison demonstrated in this practice isn't the old question of "can AI write code," but rather a fairly mature collaboration methodology: guide reviews with concise high-level prompts, use documentation as an entry point for understanding changes, employ cross-model verification, clearly triage change severity, and maintain clear-eyed awareness of costs.
In this workflow, AI handled the "heavy lifting" of discovering deep bugs, writing tedious documentation, and executing intensive refactoring, while the human maintainer's core value was expressed through SemVer semantic judgment, design trade-off decisions, and ultimate responsibility for quality.
This is perhaps the most pragmatic face of agentic engineering today: not replacing engineers with AI, but letting engineers focus their energy where judgment truly matters. The core of "agentic engineering" lies not only in model capabilities themselves but in how to design task decomposition strategies, context management approaches, multi-model collaboration workflows, and the selection of human-in-the-loop checkpoints — the design of intervention points is particularly critical: too frequent and you eliminate the efficiency gains Agents provide; too sparse and errors can cascade through multi-step operations. Emerging best practices in the industry include: using Git commits as natural checkpoints (the Agent commits after each logical unit, and humans can git diff to review at any time), using test suite pass rates as automated quality gates, and decoupling "finding problems" from "fixing problems" into separate Agent sessions to avoid context pollution. Simon's 34 commits exemplify the typical intervention pattern of "fine-grained commits + asynchronous review." These are all new engineering competencies that practitioners need to continuously explore and accumulate through practice. Simon's real-world case study is a complete demonstration of this new type of engineering capability.
Related articles

Laptops: The Last Bastion of Plaintext Secrets
Developer laptops are the last security blind spot for plaintext secrets. This article analyzes risks in .env files, shell history, and tool configs, offering practical solutions like OS keystores, dynamic injection, and short-lived credentials.

Go Microservices in Practice: Detailed Architecture for E-Commerce, AI Agent, and IM System Integration
Deep dive into integrating e-commerce, AI Agent, and IM systems under Go microservices architecture, covering unified auth, gRPC, componentized Agent engines, and group chat bots.

X Platform's Recommendation Algorithm Caught Filtering Brazilian Election Content, Reigniting Algorithm Transparency Debate
X (formerly Twitter) was found filtering Brazilian election content in its For You feed, sparking debate over algorithm transparency and free speech.