The Age of Accountable Agents: Building Trust in Your AI Automation\n\nThe air around AI feels different this "Long Hot A.I. Summer." Big tech is pouring billions into development-Elon Musk's legal battles, Meta reassigning 7,000 employees to focus on AI-it's a high-stakes, high-energy environment. But for us, building powerful AI agents on consumer hardware, this moment isn't just about raw computational power or complex models. It's about something more fundamental: trust.\n\nThe recent news cycle offers a stark reminder of the ethical considerations, user control challenges, and privacy implications that come with advanced automation. From a papal encyclical discussing AI's moral implications to significant settlements over hard-to-cancel subscriptions, and even debates around nationwide data collection, the narrative is clear: we're entering the Age of Accountable Agents. And as developers, especially those focused on local, user-centric AI, we have a unique opportunity to lead the charge.\n\n## Trust Through Transparency: The AI Encyclical's Echo\n\nWhen you hear about an Anthropic co-founder discussing AI ethics with the Pope, it's a signal that the impact of our work extends far beyond our terminals. AI agents, by their nature, automate decisions. For these agents to be truly valuable and accepted, they must be transparent.\n\nWhat does transparency mean for an agent running on your hardware? It means:\n\n* Clear Decision Paths: Can a user understand why their agent took a particular action? If your agent automatically categorizes emails, can it explain its reasoning?\n* Auditable Logic: Even if not a full "explanation," the underlying logic should be inspectable. This doesn't mean revealing proprietary secrets, but designing agents where state changes and rule applications are explicit.\n\nConsider an agent designed to manage your smart home devices. Instead of a black box, you could implement a simple logging mechanism:\n\n
python\nclass SmartHomeAgent:\n def __init__(self, name):\n self.name = name\n self.log = []\n\n def act_on_temperature(self, current_temp, desired_temp):\n if current_temp > desired_temp + 2:\n action = "Turning on AC"\n self.log_action(action, f"Current: {current_temp}°C, Desired: {desired_temp}°C")\n # ... actual AC control code\n elif current_temp < desired_temp - 2:\n action = "Turning on Heater"\n self.log_action(action, f"Current: {current_temp}°C, Desired: {desired_temp}°C")\n # ... actual Heater control code\n else:\n action = "No action needed"\n self.log_action(action, f"Current: {current_temp}°C, Desired: {desired_temp}°C")\n return action\n\n def log_action(self, action, details):\n self.log.append(f"[{self.name}] {datetime.now()}: {action} - {details}")\n\n# Usage\nagent = SmartHomeAgent("ClimateControl")\nagent.act_on_temperature(25, 22)\nprint(agent.log)\n
\n\nThis basic logging provides a human-readable trail, fostering trust by showing, not just doing.\n\n## User Autonomy, Not "Hard-to-Cancel": Learning from Shutterstock\n\nThe $35 million settlement Shutterstock faced over difficult subscription cancellations is a potent lesson: users demand control over automated systems. For AI agents, this translates directly to how we design interaction and management. Your agent shouldn't feel like a digital trap.\n\nKey design principles for user autonomy:\n\n* Explicit Opt-in/Opt-out: Clear consent for agent actions and data usage.\n* Easy Pause and Stop: Users must be able to halt or reconfigure an agent's operation immediately.\n* Understandable Configuration: Agent settings should be accessible and intuitive, not buried in obscure files.\n\nThink about how your agent's lifecycle is managed. Here's a conceptual AgentController:\n\n
python\n# pseudo-code for an AgentController\nclass AgentController:\n def __init__(self, agent):\n self.agent = agent\n self._running = False\n\n def start(self):\n if not self._running:\n print(f"Starting {self.agent.name}...")\n self._running = True\n # thread or process start logic for agent.run()\n self.agent.start_service()\n\n def pause(self):\n if self._running:\n print(f"Pausing {self.agent.name}...")\n self._running = False\n self.agent.pause_service()\n\n def stop(self):\n if self._running:\n print(f"Stopping {self.agent.name} permanently...")\n self._running = False\n self.agent.stop_service()\n # Clean up resources\n\n def configure(self, new_settings):\n print(f"Configuring {self.agent.name} with new settings.")\n self.agent.update_settings(new_settings)\n\n# When you're building your agents, consider how these controls are exposed to the user.\n
\n\nFor more effective agent management, especially concerning permissions and operational boundaries on local hardware, check out AgentGuard. It helps you build in these essential controls from the ground up.\n\n## Privacy by Design, Not by Accident: The FBI's Data Ambition\n\nThe FBI's desire for nationwide license plate reader access is a stark reminder of the sheer scale of data collection possible today. For local AI agents, privacy should be a default setting, not an afterthought.\n\nWhen designing your agents, prioritize:\n\n* Local-First Processing: Perform computations and store data on the user's device whenever possible.\n* Data Minimization: Only collect and process the data absolutely necessary for the agent's function.\n* Transparent Data Policies: Clearly communicate what data an agent uses, why, and whether it ever leaves the device.\n\nBuilding agents for consumer hardware gives us a distinct advantage here. We can champion local intelligence and ensure that user data stays private by default, not by policy fine print.\n\n## Architecting for Clarity: The Lisp Connection\n\nThe Lisp family of languages (Common Lisp, Racket, Clojure) are hyperpolyglots for a reason: their power in symbolic computation and metaprogramming encourages clarity in expressing complex logic. While you might not be writing your agent in Emacs Lisp, the principles of clear, inspectable, and modular design are paramount.\n\nAn agent with well-defined modules for perception, decision-making, and action is easier to debug, understand, and, crucially, to trust. Avoid monolithic codebases where an agent's reasoning is opaque.\n\n
python\n# Conceptual Agent Architecture\nclass AgentBrain:\n def __init__(self, perception_module, decision_module, action_module):\n self.perception = perception_module\n self.decision = decision_module\n self.action = action_module\n\n def run_cycle(self, environment_data):\n perceived_state = self.perception.process(environment_data)\n desired_action = self.decision.evaluate(perceived_state)\n self.action.execute(desired_action)\n return desired_action # For logging/traceability\n\n# Each module can have its own transparent logic, making the overall agent's behavior understandable.\n
\n\n## The Intentional Click: Feedback Loops and Refinement\n\nEven a seemingly simple site like clickclickclick.click can serve as a quirky reminder of direct user interaction. How do your agents confirm intent? How do they solicit feedback effectively? It's not about mindlessly automating every single interaction, but about designing clear, intentional communication channels between the user and the agent.\n\nConsider points where your agent might ask, "Did I do that correctly?" or "Is this what you intended?" rather than just assuming. This explicit feedback loop refines the agent's understanding and reinforces the user's sense of control.\n\n## Building for a Trustworthy AI Future\n\nThe AI revolution is here, and it's happening everywhere, from the largest data centers to the devices in our pockets. As developers crafting AI agents for consumer hardware, we stand at a critical juncture. We have the unique opportunity-and responsibility-to build agents that are not just intelligent and efficient, but also trustworthy, accountable, and respectful of user autonomy and privacy.\n\nThis summer's "AI gold rush" shouldn't just be about speed; it should be about quality, ethics, and user-centric design. By focusing on transparency, control, and privacy by default, we can ensure our AI agents truly empower, rather than overwhelm, the people who use them.\n\nTo help manage these critical aspects of your agent's lifecycle, from permissions to operational safety, explore AgentGuard. It's designed to support you in building the next generation of conscientious AI automation. Start building agents that earn trust, today.\n
Note: I wrote this in 2018, while I was helping a young startup and convincing them to open source their core application. If I were to write it nowadays, in 2024, I’d elaborate a bit more, including James Governor’s evergreen remark about how “You can make money with open source, but it’s extremely hard to make money from open source.” (source). Leaving it as-is, however.*
I’ve been advising a startup on the data transformation space. As part of this, we re-wrote the core engine in Clojure. The new version is, at the worst case, 16 times as fast in the same hardware, and in some cases over 200 times faster. And it does it in a fraction of the lines of code.
We did this in under 3 months of part-time work. We couldn’t focus our entire attention on it, as we had other concerns as well - I was involved with general team and management tasks, and the second developer was helping on other internal projects as well. To further raise the bar: we had to keep it functionality-compatible with the current version, so I had to get acquainted with the existing feature set, and it was the other developer’s first Clojure project.
Clojure made our lives so much easier. But this is not a post about why Clojure is cool.
I’ve been arguing about why they should open source anything that is not enterprise-specific, including this layer. There’s many advantages, which I won’t go over right now, but there’s also a looming threat.
Layers are getting commoditized faster and faster. More and more, there is demand for people who are good at wiring things together (beyond the gem install hairball approach), or tools that help with that wiring.
I suspect that’s a big part of what’s driving how many companies like Seldon are going open source-first, or how Unreal opened their code as a way to compete with Unity.
“what’s driving how many… are going” is a doozy
Remember: This rewrite took about 3 man-months, with our attention pulled in multiple directions, while we strove to remain feature-compatible with the old engine. You have to assume anyone else who has the technical chops but doesn’t have that baggage can do it as well. Do you want to be disrupted by some motivated, random person who thought what you were doing was cool, but neither needed your entire feature set nor wanted to be shackled to your cloud version?
Better to commoditize yourself before someone else does it to you. You get to have a say in how it happens, use it as fuel to propel you somewhere new. And you get to tell your customers “if you think this thing we give away is cool, you should see the part we charge for”.
OK, this is going to be a tough one. Strap down, even if it pisses you off at first - I wouldn’t be writing it if I didn’t think it was helpful.
Stop me if you’ve heard this one before
A programmer has something he wants to build.
He finds a framework.
It looks easy enough to get into. There’s a few tutorials available, which show step by step how to build a basic application of just the right type.
He’s happy. He feels really productive. CRUD just “drops out” of the framework’s design. It’s like it was created with his problem in mind.
Then he’s done with the tutorials. He starts plugging in his own, specific requirements. Every so often he hits a snag, but that’s OK, he can just twist the framework’s arm a bit, right?
It all goes well for a while. Then he hits a big one. The framework doesn’t do quite work the way he wants it to.
He posts a question. Maybe he doesn’t get an answer. Maybe he does, and he’s told it’s not within the benevolent dictators’ design parameters.
Crap. Oh well. He can start working around the decisions that don’t match his application. This will add some development overhead. At some point it’ll be enough that he has to consider his options.
That’s OK. He has the source. He can fork it, and do his custom stuff - he knows it well enough by now. He’ll lose the upstream fixes, unless he puts in some real effort into integrating them, but at least he doesn’t have to ditch the codebase.
This will tide his team over until the re-write, which they’ll build upon a different framework. One that looks like it better suits their needs.
Status quo
That’s just the way things are, right?
If you take someone’s code, they have made some decisions for you. Sometimes the decisions work in your favor, sometimes they don’t.
There’s an alluring side to this. The main reason why a framework might feel solid at first is because someone made a lot of decisions for you - one might say such a framework is opinionated. Those decisions seem to make sense, and they get you from zero to CRUD in just a few steps.
If you happen to be building the exact type of application that the framework creators have in mind, if their way of thinking immediately clicks with you, that’ll take you a long way.
But when you try to move away just bit from the set of problems they decided to address, you realize that those choices accrete. On a solidly integrated framework, you don’t get to pick and choose. Pulling away even a single one requires considerable effort.
Most of the time, either you take it or you leave it.
The not-framework approach
Frameworks expect to have an answer to most of your fundamental questions. Can you really rely on them to do that?
Consider an alternate path.
At first glance, Clojure’s Luminus is not as solidly integrated as other alternatives. For every potential component, you get to pick and choose - and never mind PostgreSQL vs. MongoDB vs. Datomic.
It defaults to Migratus for database migrations… but you can just pull it out and use any alternative.
Ring provides a nice abstraction for HTTP, but which web server do you want? Immutant or HTTP Kit?
Do you want ClojureScript support? If so, do you want Reagent or Om? And what’s the difference between those two anyway?
Even once you’ve made a decision, chances are you won’t find any step-by-step zero-to-CRUD tutorials for your specific combination.
So… how does this help again?
Control
No, Luminus is not making any decisions for you. The very few areas where it provides a single choice, like Selmer for templating, are orthogonal to your application’s design.
Unlike the frameworks in fashion on other languages - or even some that are starting to trend for Clojure - it’s not opinionated. One could say it’s open minded.
Here’s what happens when you create a new Luminus application.
You’ll have to tell it which options you want included. A list of the options is available on the site.
Luminus will generate a new template from those options, every option potentially pulling in a few libraries.
It will also include some reasonable defaults and glue code, to save you time.
While it’s happy to show you its conventions with the templates that it generates, you won’t find it making any choices for you in advance. But more importantly, at no point does it try to wrestle control of those decisions from you.
It lets you make up your own mind.
Decision independence
Each library Luminus includes does make some decisions internally, for obvious reasons. But these are mechanical in nature, and given that Luminus doesn’t want its basic components to be strongly fused together, you are free to swap out any particular part.
Think of it as the bevy of functions you have on Clojure. Every one of them provides a piece of independent, re-usable functionality. Yes, the internal decisions of how the functions were implemented are made for you in advance, but it’s up to you to decide how to put them together to build your application, or if even you’d rather use an external library.
Learning curve
Yes, this means there will be a slightly steeper learning curve. You’ll need to know what these choices mean, before you can make an informed decision.
That means that on day-1, you’ll feel less productive than how you would on an opinionated framework, because you don’t get to coast on decisions someone else has made for you.
If you are looking for day-1 productivity, it may not be for you.
I’m not. I’m looking at life time value.
After using this loosely-coupled approach on projects for a couple of years, I’ve realized two things.
I like choosing
I get to replace any single part of the system, if I find it doesn’t suit my needs.
I may not need to. Hopefully I made the right decision up-front. But if for whatever reason I didn’t, I know that no other choice is tightly integrated with it, so I can gradually pull it out and replace it - I don’t need to switch frameworks, or fork it, or live with it.
I get to change my mind.
I no longer dread updates
Up until a few months ago, YeSQL was the standard way to access SQL databases in Luminus. When YeSQL support slowed down, Dmitri Sotnikov decided to replace it with HugSQL, which follows the same approach but takes it further and is actively developed.
Want to guess how many existing Luminus projects it affected?
Zero.
How can I state that with certainty?
Because it couldn’t possibly have affected a single one. Your choices are only for which libraries is your initial template generated from. After that, you are free to decide which of these do you want to update at any point, depending on if it suits you or not.
Any changes to future Luminus choices will have zero impact on you.
Productivity trade-offs
This means initially you’ll be taking baby steps. You may feel like an idiot as you go about figuring things out.
We all hate that. I get it.
It’s part of what makes frameworks so appealing when facing a new platform: they promise you’ll hit the ground running. Nobody likes to crawl when they’re used to just zoom by basic tasks.
But when changing contexts, we have to take baby steps first and then go on from there, because every decision we make is a trade-off.
When choosing a loosely-coupled approach, you’re saying that day-1 productivity is less important than long-term maintenance and total application lifetime productivity.
Personally, I’d rather spend time up-front understanding the choices I’m making. If I don’t have fundamental decisions made for me in advance, I have to look at what the options are, and can decide to change some pieces.
This understanding increases transparency, which in turn enhances my comprehension of the very foundations that I’ll be building upon for months or years.
Given I spend more time on application support and extension than on the first few days, it’s a trade-off I’m happy to make.
I’m happy to announce that the Clojure book has gotten a bit better! A few days ago, I received an email from someone named Alex Bedner. He read my Clojure book from start to finish and was kind enough to share some corrections. I’ve implemented them, and you can find the updated book here: https://clojure-book.gitlab.io/
I did try to upload the print copy to Amazon, but for some reason they require additional verification from my side — and I’m not entirely sure what that entails. I’m not great with bureaucracy, so getting the Amazon listing sorted will take some time. In the meantime, Alex Bedner’s proofread version of the Clojure book is available at the link above. I’m deeply grateful to him.
There’s still a lot to do, and I remain committed to Clojure. Here are the things I’d like to finish in the book:
Debugging with Calva (https://clojure-book.gitlab.io/book.html#_debugging_with_calva) — I need to write this section, with plenty of screenshots so readers know how to debug with Calva. At the very least, I’d like to link to the Calva website, where the creator has put together some excellent videos.
I’d also like to add a section on Paredit at some point — we’ll see how that goes.
Finally, I’d love to use LLMs to help proofread and polish the book further. English isn’t my first language, and I’ve found that AI tools have been genuinely helpful in improving the quality of my writing lately.
Once again, thank you to everyone who has said kind words about the book, pointed out corrections, offered criticism, or praised it. All of it is valuable input, and I’ll make the most of every bit of it.
Join us for the largest gathering of Clojure developers in the world! Meet new
people and reconnect with old friends. Enjoy two full days of talks, a day of
workshops, social events, and more.
September 30 – October 2, 2026
Charlotte Convention Center, Charlotte, NC
After a decade away, EuroClojure returns to mark twenty years of Clojure. Join
us in Prague for three days of workshops, talks, and thoughtful conversation.
phel-symfony-demo - Symfony + Phel demo: Ring-style handlers over plain maps, no ORM
stube - Web framework inspired by Seaside and UnCommonWeb
clj-native-agent - Structural, semantic, and surgical skills for Clojure AI agents. Mimics expert practitioner workflows for code discovery, debugging, and editing using Babashka and rewrite-clj.
clj-watson6.1.0 - A Clojure tool that checks for vulnerable dependencies
aleph0.9.8 - Asynchronous streaming communication for Clojure - web server, web client, and raw TCP/UDP
awesome-backseat-driver1.0.13 - Plugin marketplace for Clojure AI context in GitHub Copilot: agents, skills, and workflows for REPL-first interactive programming with Calva Backseat Driver
phel-lang0.39.0 - A functional, Lisp-inspired language that compiles to PHP. Inspired by Clojure, Phel brings macros, persistent data structures, and expressive functional idioms to the PHP ecosystem.
This is the second of six reports from the developers who are receiving annual funding for 2026. Thanks to everyone for supporting their work and important contributions to the Clojure community.
Bozhidar Batsov: nREPL, Clojure Mode, ts-mode, Orchard, CIDER, and more Clojure Camp: Badges, nano-conj, excercises Eric Dallo: eca, eca-desktop, clojure-lsp, clojure-lsp-intellij Jeaye Wilkerson: Jank compiler architecture and optimization Michiel Borkent: babashka, ClojureScript async/await, Rebel, squint, and much more
Bozhidar Batsov
2026 Annual Funding Report 2. Published May 1, 2026.
The last two months have been some of the busiest I’ve had in clojure-emacs
land in a long while. No big “X.Y” CIDER release to point at yet, but there’s
been a steady stream of important work across most of the sibling projects.
Below are some of the highlights.
clojure-mode 5.22 and 5.23
Two clojure-mode releases back-to-back, after a long stretch of relative
quiet:
5.22 (March 3): a big bug-fix dump — clojure-sort-ns no longer mangles :gen-class, clojure-thread-last-all doesn’t eat closing parens into line comments, clojure-add-arity preserves arglist metadata, letfn bindings get a function face, and edn-mode indents data like data. On the feature side: a new clojure-preferred-build-tool for projects that have several build tool files lying around, a dedicated clojure-discard-face for #_ forms, and project root detection for ClojureCLR’s deps-clr.edn.
5.23 (March 25): adopts the modern indent spec tuple format (((:block N)), ((:inner D)), …) that clojure-ts-mode and cljfmt already use. The legacy format (integers, :defn, positional lists) still works, but it’s slated for removal in clojure-mode 6. There’s a new public clojure-get-indent-spec API, and CI moved off CircleCI onto GitHub Actions.
Unifying the indent format across our Clojure tooling has been on my todo list
for years - happy to see it finally happen!
clojure-ts-mode
No release in this window, but I poured a lot of effort into the test suite -
proper indentation and font-lock test coverage, a new configuration option
test suite, and integration tests against sample files. Boring infrastructure
work, but it’s the kind of thing that pays compound interest as the mode keeps
maturing.
Side note: In the past several months I’ve spent a bit of time hacking on neocaml and my work there provided quite a bit of inspiration for improvements to clojure-ts-mode. (and, of course - clojure-ts-mode inspired me to create neocaml in the first place)
Orchard 0.41
Orchard v0.41.0
shipped on April 13 with a rewrite of orchard.xref, a refactor of
orchard.indent, and a test modernization pass. I chipped in some smaller
fixes - a cycle guard in fn-transitive-deps, validating input to
stacktrace/analyze, compiling a hot regex once instead of on every call, and
standardizing the -test suffix across the test suite.
We’ve also migrated from Lein to tools.deps for Orchard and probably we’ll continue in this direction for the rest of our Clojure projects in the nREPL and Clojure Emacs GitHub orgs. No rush on that front, though.
cider-nrepl 0.59
cider-nrepl v0.59.0
landed on April 14. The headline change from my side is that the ops are now
properly namespaced (#710) -
something that’s been sitting in the issue tracker for a very long time. CIDER
on master already speaks the new namespaced ops with a fallback to the legacy
names for older cider-nrepl versions, so upgrades should be painless.
Other notable bits: Compliment and clj-reload are no longer shaded, the test
middleware properly binds *report-counters*, and we extract inline maps from
composite Lein profiles correctly.
refactor-nrepl
refactor-nrepl got a serious modernization push in April - no release yet,
but a lot of cleanup landed:
Dropped http-kit in favor of the JDK’s built-in HTTP client (one fewer dependency, one less thing to break).
Restored hotload-dependency on top of tools.deps.
A new def-op macro to simplify how ops are defined and how errors are handled.
A shared test session helper, a make lint target, and modernized CircleCI executors.
A full dependency bump.
Expect a refactor-nrepl release once the dust settles.
clj-refactor.el
Just a small cleanup pass
to align with the new Emacs 28+ baseline. clj-refactor is in maintenance mode
these days, but I’m trying to keep it healthy.
Eventually I’ll be taking a closer look at moving some of its functionality to clojure-mode and CIDER, but there’s more ground-work that needs to be done first.
CIDER
No CIDER release this cycle, but master accumulated a lot of structural
work that will land in CIDER 1.22:
nrepl-client.el decoupled from CIDER. It no longer depends on sesman or any of CIDER’s UI layer, which makes the nREPL client genuinely reusable outside CIDER. I also split cider-connection.el into cider-session.el + cider-connection.el and decoupled cider-eval.el from cider-repl.el.
cider-jack-in got a serious overhaul: a proper jack-in tool registry, unified entry points around shared helpers, TRAMP fixes, sane runtime defaults, and cider--resolve-command now actually works over TRAMP.
A pile of resilience fixes for the nREPL client: tear down the SSH tunnel on abnormal client disconnect, spawn the tunnel without a shell, plug request-id leaks in raw response handlers, stop response-handler errors from poisoning the response queue, and put a FIFO cap on nrepl-completed-requests so it can’t grow without bound.
Namespaced ops everywhere, matching cider-nrepl 0.59 - with a fallback to the legacy op names so older cider-nrepl keeps working.
A new nrepl-make-eval-handler with a keyword-arg API (and removal of the trivial wrappers that used to live around it).
Mode-line spinner while tests are running, which is a small thing but I missed it every time I ran a long test suite.
Lots of test coverage backfill on the client side.
This is shaping up to be one of those releases that doesn’t look flashy on the
outside but cleans up a ton of long-standing internal mess. The decoupling work
in particular has been on my mind for years.
I’ve also started working on experimental support for prepl in CIDER. I’m not sure if this will ever land
or be properly supported, but it’s been a fun side quest to me to ensure that CIDER logic is decoupled properly
from its bundled nREPL client that I plan to eventually spin out of CIDER.
MrAnderson
Slightly outside the clojure-emacs org, but related: I’ve been helping
MrAnderson get back on its
feet. Three PRs landed in March/April:
MrAnderson is what cider-nrepl (and others) use to shade dependencies, so a
healthy MrAnderson is in everyone’s interest. There’s more to come here!
What’s next
CIDER 1.22 once the jack-in and decoupling work settles, a refactor-nrepl
release to ship all the April cleanup, and hopefully a fresh MrAnderson cut
soon after.
As always - thanks to everyone who pitched in, especially Sashko Yakushev who
once again did much of the the heavy lifting on cider-nrepl and Orchard.
And a HUGE THANKS to the members of Clojurists Together for supporting my Clojure OSS work! You rock!
Clojure Camp
2026 Annual Funding Report 2. Published May 12, 2026.
Some of our CT funds are being set aside to support three efforts this year:
sponsoring conference attendance for new Clojurians,
supporting Clojure and non-Clojure meetups with a “pizza fund” (WIP),
hosting an experimental nano-conj (in-person multi-day open-ended hack-on-clojure “retreat”) (WIP)
Continued on badge system, mostly wrestling with display of the “learning graph”
Other happenings
Hosted an in-person workshop at the Recurse Center (with another planned)
Started a new book club reading Knesl’s “Applied High Order Functions in Clojure”
Early planning for our presence at Conj this year (may be doing a “Learning Together” track on the workshop day, featuring Mobbing and Pairing sessions)
Raf has a major commitment finishing up in May, so June+July should see a big push on Camp projects (and resuming regular Mobs)
Eric Dallo
2026 Annual Funding Report 2. Published May 8, 2026.
Excited 2 months of lots of work and help from Clojurians! We had improvements in eca and clojure-lsp mainly, and new projects as well.
ECA keeps growing a lot, receiving lots of contributions, with more than 800 stars already I’m planning a stable release soon, in these 2 months we had lots of releases with ton of stuff, so I will focus on the main highlights:
0.109.1 - 0.131.1
Plugins: New plugin system to load external configuration from git repos or local paths, with an official marketplace at plugins.eca.dev. Plugins can provide skills, MCP servers, agents, commands, hooks, rules and arbitrary config overrides, managed via /plugins, /plugin-install and /plugin-uninstall.
Remote web control: New remote web control server for browser-based chat observation and control via web.eca.dev, allowing you to observe and drive ECA chats from any browser.
Trust mode: Clients can now auto-accept tool calls that would require manual approval, with regex patterns support for fine-grained shell_command approval.
Task tool: Built-in task tracking tool to let the agent plan and follow multi-step work more reliably.
Background shell commands: New background parameter on shell_command and a dedicated bg_job tool to manage long-running processes like dev servers and watchers.
Chat list, open, fork and /model: New chat/list, chat/open and /fork commands let clients render a chat sidebar and clone existing conversations, plus a new /model command to switch model mid-chat.
Message flags: Named checkpoints inside a chat for resuming and forking from a specific point, with full chat history preserved across compactions via tombstone markers.
ask_user tool: LLMs can now ask the user questions with optional selectable options, fully integrated with hooks and trust modes.
Image generation: Support for OpenAI’s built-in image_generation tool via the Responses API, including image edits across turns and MCP tools that return image content.
Prompt steering: New chat/promptSteer notification to inject user messages into a running prompt at the next LLM turn boundary, without stopping it.
MCP improvements: New mcp/addServer, mcp/removeServer, mcp/updateServer, mcp/enableServer and mcp/disableServer requests to manage MCP servers at runtime, plus much better OAuth spec compliance and a switch from the Java SDK to plumcp.
More providers and models: Added LiteLLM, LM Studio, Mistral and Moonshot as built-in providers with login support, Claude Opus 4.7, deepseek-v4-pro, gpt-5.4 and gpt-5.5 variants, and GitHub Enterprise Copilot.
Since ECA has been pretty stable and built in top of a nice extensible protocol, it worked so well that we decide to offer the same server capabilities to a Desktop client, similar to Claude Desktop but reusing the same server, this makes possible to have the same experience without an Editor, especially useful for non techinical people in price for a thin layer connecting to the server.
That’s a lot for ECA and all part of the amazing community that’s been activelly helping with issues, feedback and contributions.
We had some important bumps with lots of fixes, new code actions and contributions! The main highlight here is the arrival of performance tests in the project, which is a long waited thing which would help unblock performance optmizations in clojure-lsp since now we have a way to reliable know a mean, p90, p80 of how much time clojure-lsp spend on its features like initialization.
Support find-definition for fully qualified vars even when the namespace is not explicitly required. #2028
Fix create-test code action appending a duplicate deftest when one with the matching name already exists, now navigating to the existing deftest instead. #2274
Change the default of :clean :ns-inner-blocks-indentation from :next-line to :keep, so clean-ns (including the automatic run after add-missing-libspec, add-require-suggestion, add-missing-import, and move-form) no longer reflows the :require/:import block when the user has not configured an indentation style. Users who want the previous behavior can set :clean :ns-inner-blocks-indentation :next-line explicitly. #2261
Fix add-missing-require refer suggestions leaking across languages, so a .clj file no longer offers refers defined only in .cljs files (and vice versa). #2271
Add :private-by-default-on-extract? setting to control whether extracted functions and defs are private by default. #2258
Measure performance of code actions
Avoid incorrect circular dependency errors from :as-alias by working around clj-depend bug.
Fix inline-def to work with defs with metas.
Bump clj-kondo to 2026.04.16-20260503.191510-9.
bump up timeout for code action performance measurement, include p90 measurement #2236
implementation of inline function. #1827
Fix initialization crash when a source file has syntax errors (e.g. unbalanced parens) by using safe parser in unused-public-var linter’s :gen-class check. #2242
Bump rewrite-clj to 1.2.54.
implement move to :let refactoring #1732
Measure performance of didOpen and didChange
if code-action selection end-position args aren’t provided, don’t try to use them #2276
add unit tests for command actions location args #2279
New code actions: replace :refer with :as and replace :as with :refer, with support for merging into existing :refer vectors.
Implement createServerInstaller and createLanguageServerSettingsContributor on ClojureLanguageServerFactory. Newer LSP4IJ versions added these as default interface methods, but our def-extension/gen-class-backed factory always overrides interface methods, so explicit stubs are required.
Fix slurp-action-test against newer LSP4IJ versions. Bump clj4intellij to 0.9.0 and switch the test fixture to setup-heavy so the project base has a real filesystem path (LSP4IJ’s FileSystemWatcherManager calls VirtualFile.toNioPath(), which throws on the in-memory TempFileSystem used by light fixtures).
Fix new namespace creation incorrectly creating files under the absolute host filesystem path. #83
Fix QuoteHandler compile error by merging BAD_CHARACTER into the quote-handler TokenSet.
Fix auto closing single quotes.
Improve CI to have plugin zips in all releases, avoiding wait for Jetbrains approval.
Jeaye Wilkerson
2026 Annual Funding Report 2. Published May 8, 2026.
Howdy folks! Thank you so much for the sponsorship this year. For the last two
months, I have been focused on compiler architecture and optimization for jank.
On the compiler architecture side, I have designed and implemented a custom
intermediate representation (IR) for jank programs which sets the stage for
writing Clojure-specific optimization passes. This IR operates at the level of
Clojure’s semantics, which is much higher level than LLVM IR, and so we are able
to perform optimizations which LLVM could never do for us.
On the optimization side, I have taken the first benchmark of many, recursive
fibonacci, and I have optimized it to be nearly twice as fast as Clojure JVM,
for the same exact code. This benchmark is the first of many and I will be
following up with more benchmark optimization results in the coming two months,
using our new IR as a platform for optimizing jank’s performance.
To read all about the details of jank’s IR and the benchmark optimization, take
a look at this blog post:
Michiel Borkent
2026 Annual Funding Report 2. Published May 11, 2026.
In this post I’ll give updates about open source I worked on during March and April 2026.
I’d like to thank all the sponsors and contributors that make this work
possible. Without you, the below projects would not be as mature or wouldn’t
exist or be maintained at all! So a sincere thank you to everyone who
contributes to the sustainability of these projects.
Babashka Conf 2026 was held on May 8th in the OBA Oosterdok library in Amsterdam! David Nolen, primary maintainer of ClojureScript, was our keynote speaker. We’re excited to have Nubank, Exoscale, Bob, Flexiana and Itonomi as sponsors. Nubank and Exoscale are hiring. Wendy Randolph was our event host. For the schedule and other info, see babashka.org/conf.
The day after babashka conf, Dutch Clojure Days 2026 was also held - so it was a great weekend in Amsterdam!
Hope to have seen many of you there!
Projects
In the last two months I spent significant time organizing babashka conf, but made progress in several projects as well.
My upstream work to enable async/await in ClojureScript was merged in the beginning of March. The implementation mirrors squint. Thanks David for reviewing and merging. Also deftest now supports an ^:async annotation so you can use async/await and don’t need to mess around with the cljs.test/async macro anymore:
I’ll be presenting this work at the Dutch Clojure Days.
Rebel-readline is now bb compatible. The work involved mainly exposing more JLine stuff and making sure rebel-readline didn’t hit any internal JLine APIs.
One step to drive this to completion was to make a dependency, compliment, bb compatible. Thanks both to Bruce and Alexander for the cooperation.
Squint now supports cljs.test and multimethods! clojure-mode was ported to use the new cljs.test.
On the cream front, I put in effort to make the binary smaller and have been keeping up with the new GraalVM EA releases. I’ve been posting bug reports to the crema maintainer. Currently there’s still an unfixed bug around core.async that I have trouble reproducing in pure Java. I also added lots of library tests to CI so I can ensure stability in the long run. For now it remains experimental, but the direction is promising.
A performance PR to weavejester/dependency speeds up depend, depends? and topo-sort significantly, so clerk notebooks render faster.
The cljfmt library, also by @weavejester, now fully runs from source in babashka. The Java diff library that wasn’t bb-compatible was replaced with text-diff, but only for the babashka path. The JVM build of cljfmt still uses the original Java diff library, with a possible switch later once text-diff has matured.
Several SCI fixes were made to improve Clojure compatibility between babashka and Clojure. E.g. records can now support extending to IFn which was a blocker for some Clojure libs that tried to run in bb so far.
Clj-kondo 2026.04.15 got a few new linters thanks to @jramosg for stewarding most of these. It also has better out of the box potemkin support, and @alexander-yakushev contributed a wave of performance improvements.
Updates per project below. Bullets are highlights; see each project’s CHANGELOG.md for the full list.
babashka: native, fast starting Clojure interpreter for scripting.
Released 1.12.216, 1.12.217 and 1.12.218
Support rebel-readline as external REPL provider:
Add proxy support for Completer, Highlighter, ParsedLine, Writer, Reader
Add clojure.repl/special-doc and clojure.repl/set-break-handler!
Add clojure.main/repl-read
Add org.jline.reader.Buffer to class allowlist
Add clojure.java.javadoc namespace with javadoc available in REPL #1933
Fix (doc var), (doc set!) and other special forms #1932
Support (source inc) and (source babashka.fs/exists?) for built-in vars #1935
Support BABASHKA_REPL_HISTORY env var for configurable REPL history location #1930
Fix deftype and defrecord inside non-top-level forms (e.g. let, testing) #1936
#1967: expose clojure.data.xml.tree/{flatten-elements,event-tree}, clojure.data.xml.event record constructors, and clojure.data.xml.jvm.parse/string-source
#1969: include java.net.Proxy and java.net.Proxy$Type Java classes (@jeeger)
#1939: disable JLine backslash escaping/shell history commands (@bobisageek)
Performance improvements for math operations and for calling functions on locals
Add many new classes to reflection config: java.lang.reflect.Constructor, java.lang.reflect.Executable, java.util.stream.Collectors, java.util.Comparator (for reify), and more
Bump JLine to 4.0.12, cheshire to 6.2.0, nextjournal.markdown to 0.7.255, edamame to 1.5.39, data.xml to 0.2.0-alpha11, jsoup to 1.22.2, rewrite-clj to 1.2.54, tools.cli to 1.4.256, transit-clj to 1.1.357, fs to 0.5.32
clj-kondo: static analyzer and linter for Clojure code that sparks joy.
Released 2026.04.15
#2788: NEW linter: :not-nil? which suggests (some? x) instead of (not (nil? x)), and similar patterns with when-not and if-not (default level: :off)
#2520: NEW linter: :protocol-method-arity-mismatch which warns when a protocol method is implemented with an arity that doesn’t match any arity declared in the protocol (@jramosg)
#2520: NEW linter: :missing-protocol-method-arity (off by default) which warns when a protocol method is implemented but not all declared arities are covered
#2768: NEW linter: :redundant-declare which warns when declare is used after a var is already defined (@jramosg)
#1878: support potemkin’s import-fn, import-macro, and import-def
#2498: support new potemkin import-vars:refer and :rename syntax
Performance optimizations across many linting paths (@alexander-yakushev) and hook-fn lookup caching to avoid repeated SCI evaluation
Add type support for pmap and future-related functions (future, future-call, future-done?, future-cancel, future-cancelled?) (@jramosg)
#2762: fix false positive: throw with string in CLJS no longer warns about type mismatch (@jramosg)
#2770: linter-specific ignores now correctly respect the specified linters
#2773: align executable path for images to be /bin/clj-kondo (@harryzcy)
#2621: load imports from symlinked config dir (@walterl)
#2798: report correct filename and error details when StackOverflowError occurs during analysis
Followed each GraalVM EA release: EA21 shrunk the binary to ~175MB, EA22 brought a virtual-thread fix, EA23 fixed the forkjoin segfault, EA24 finally allowed re-enabling clojure.core.async-test
Added smoke tests for httpkit, nextjournal/markdown, clj-yaml, core.async ioc-macros
Updated 10M loop benchmark numbers for EA21
Added Windows test status notes (still some failures on EA22)
Fix with-meta now preserves callability when applied to a function
#783: auto-load macros from .cljc files via :require (no need for :require-macros); resolve qualified symbols from macro expansions
#784: resolve transitive macro deps and auto-import runtime deps from macro expansion
#809: add squint.compiler/compile* and squint.compiler/transpile* which accept either a string or a sequence of pre-parsed forms, skipping the forms -> string -> forms roundtrip for SSR use cases
#810: shorthand classes in #html / #jsx were erased when an attrs map was present without a :class key
cherry: Experimental ClojureScript to ES6 module compiler
Accept plain await as a special form, in anticipation of CLJS next
Multiple :require-macros clauses with :refer now properly accumulate instead of overwriting each other
Add cherry.test with clojure.test-compatible testing API: deftest, is, testing, are, use-fixtures, async, run-tests. Macros are compiler built-ins (shared with squint), so no :require-macros plumbing is needed in user code.
Engelberg/instaparse: submitted #242 for babashka compatibility. Required :bb reader conditionals to replace the AutoFlattenSeq deftype with plain vectors plus metadata markers, swap the Segment deftype for a reify-based CharSequence, and add a CI test runner. Open, awaiting review.
Other projects
These are (some of the) other projects I’m involved with but little to no activity
happened in the past two months.
Click for more details
scittle: Execute Clojure(Script) directly from browser script tags via SCI
A Clojure Lambda function for monitoring AWS CloudWatch Logs subscriptions, using Gatus to monitor scheduled jobs, and configuring Amazon EventBridge Scheduler to run an ECS task twice a day.
When you let an AI agent write Clojure code, you expect it to leverage the language's superpowers—the REPL's interactivity, structural editing, format-preserving code manipulation, and the rich ecosystem of wrapper libraries. Instead, what you typically see is mediocre code written slowly, as the agent makes the same mistakes every developer learns to avoid.
I discovered this the hard way.
The Setup: Vibe Coding with Observations
While building lite-crm with Claude Code, I deliberately avoided the –dangerously-skip-permissions flag. Instead, I sat beside the agent and watched it work—observing its patterns, frustrations, and failures. What I saw was an agent trained on millions of codebases but ignorant of how Clojure practitioners actually think.
Three concrete problems emerged:
Problem 1: The Wrapper Library Blind Spot
When encountering Java interop, the agent jumps straight into direct interoperability without ever asking: "Is there a Clojure wrapper library for this?"
The result: Uglier code, harder to maintain, and a missed opportunity for idiomatic Clojure.
Problem 2: Formatting Brittleness
Code formatters like cljfmt are essential—but they create a sneaky problem. When the agent modifies source and the formatter shifts indentation by a single space, the agent's subsequent str_replace operations fail due to whitespace mismatch.
The result: I watched it fail, retry, fail again, then give up and rewrite entire files. Enormous token waste.
Problem 3: Primitive Debugging
When a test failed, the agent fell back on the crudest debugging technique: add println statements, run the test, inspect output, delete the logs, restore the code. Repeat.
This is especially wasteful in a Clojure project where I've provided direct access to the REPL via the brepl CLI. The agent could inspect values interactively, test hypotheses instantly, and trace execution without touching source code. But it never did.
The Recognition
These weren't knowledge gaps. They were behavioral gaps—places where the agent's default approach conflicted with Clojure expertise.
In the context of Clojure Stack Lite (which includes proper testing harness and real database, not mocks), the agent wasn't just writing suboptimal code—it was making design decisions based on unfamiliar tools.
I decided to address this not by teaching the agent more facts, but by redirecting its behavior.
Four Skills to Close the Gap
The result is four skills, each targeting a specific behavioral pattern that distinguishes novice agents from expert Clojure practitioners:
1. clj-debug: From Logging to REPL Inspection
The Problem: Agents default to adding println, tap>, or logging statements, then running tests to inspect output.
The Pattern: In Clojure, this is backwards. The REPL lets you pin a value with def, explore its structure instantly, test hypotheses interactively—all without modifying code.
What the skill does: When you're about to debug, clj-debug redirects from logging patterns to REPL-based inline inspection. It teaches the agent to use def, keys, keyword access, and structural exploration—the actual workflow expert Clojure developers follow.
Behavioral change: From edit-test-inspect cycle to interactive REPL inspection. This is faster, non-invasive, and gives immediate feedback.
2. clj-discover: Systematic API Exploration
The Problem: When encountering unfamiliar Java classes or macros, agents jump to direct integration without exploring whether an idiomatic Clojure wrapper already exists.
The Pattern: Expert Clojure developers follow a deliberate workflow:
Search for a Clojure wrapper library first (usually there is one)
If not, inspect the Java class via reflection
For macros, expand them to understand what code they generate
What the skill does: clj-discover codifies this workflow, ensuring the agent prioritizes idiomatic libraries and systematic exploration before writing integration code.
Behavioral change: From direct interop to research-first integration. The result is cleaner, more maintainable code.
The Problem: Code formatters shift indentation by spaces, breaking text-based str_replace. The agent then wastes tokens failing repeatedly or rewriting entire files.
The Pattern: Clojure is homoiconic—code is data. Two S-expressions are semantically equivalent even if formatted differently. Expert editors handle this automatically via structural editing.
What the skill does: clj-replace compares code by structure (S-expression equivalence) rather than text, ignoring whitespace while preserving the original file's formatting style. It uses the rewrite-clj library to parse, match, and replace nodes safely.
Behavioral change: From brittle text matching to robust structural matching. Formatting variations become irrelevant.
4. clj-refactor: Mechanism/Policy Separation
The Problem: Without guidance, agents write tangled code where reusable mechanisms are mixed with business policy, creating inflexible designs that accumulate technical debt.
The Pattern: Arne Brasseur's mechanism/policy separation principle is core to building maintainable Clojure systems. Mechanism is context-free, stable, and reusable. Policy is opinionated, domain-specific, and volatile. Expert developers keep these separate.
What the skill does: clj-refactor scans code for opportunities to extract mechanisms from policy—functions where hard-coded values or implicit context can be made explicit, dependencies can be pushed to parameters, and reusable logic can be isolated.
Behavioral change: From monolithic functions to extracted, composable mechanisms. Code becomes easier to test, reuse, and reason about.
Note: Unlike clj-debug, clj-discover, and clj-replace—which activate automatically when the agent encounters problems—clj-refactor is user-initiated. You invoke it when you want the agent to analyze code for refactoring opportunities, not in response to a failure.
Why This Matters
These aren't reference manuals or API documentation. They're workflow redirects—rules that teach AI agents to think like expert Clojure developers instead of generic code writers.
The underlying philosophy is simple: A skill's value is measured by behavioral change, not knowledge transfer.
When an agent uses clj-debug, it stops adding logging. When it uses clj-discover, it checks for idiomatic wrappers before raw interop. When it uses clj-replace, formatting becomes irrelevant. When you invoke clj-refactor, the agent identifies tangled mechanisms and suggests extraction. Each skill shifts the agent's default patterns closer to expert practice.
This matters because Clojure is a language of leverage. The REPL, immutability, homoiconicity, and the functional approach all reward practitioners who use them correctly. An agent that doesn't leverage these features isn't just writing slow code—it's missing the point of the language.
The goal is simple: your AI agent shouldn't just write Clojure code—it should think like a Clojure developer. These four skills make that possible.
Snapshotting via copy-on-write is a well-trodden idea. ZFS and btrfs do it at the filesystem block layer; Neon and Aurora do it at the database page layer; Datomic and Datahike do it at the data-model layer. What differs is where the immutability lives, and that determines what you can do with the snapshots once you have them.
In Datahike, the database value itself is immutable. A datom never mutates; a query is always against a specific commit; a branch is a database value you can hand to a function. That last property changes the calculus in three ways.
First, branching is the same primitive as every other transaction. There’s no special bulk-load path, no restore mode, no control-plane operation — just a couple of small writes to storage.
Second, branches are database values you can pass to a query. The same query interface that reads the head of :db reads any historical commit on any branch. No special “as-of” mode, no separate replica.
Third, merging becomes a query. ZFS can clone a snapshot but can’t merge two of them — a filesystem doesn’t understand its own contents well enough to resolve a conflict. Datahike does: branches are database values, Datalog queries take multiple databases as inputs, so “what’s in :feature and not in :db” is a query you write. Filtering, transformation, and conflict resolution are all the same language you query the database with.
The rest walks through datahike.versioning in order, with a brief note at the end on how the same surface shows up in the other bindings.
The storage model
A Datahike database is a persistent sorted set of datoms — five-tuples of [entity attribute value transaction op]. The storage layer is persistent-sorted-set, a B-tree-based immutable data structure designed for on-disk storage of sorted runs of datoms.
What matters for branching is the persistence property: every node is immutable. A transaction that adds, retracts, or modifies datoms walks from root to leaf, creates new nodes along the changed path, and leaves the unchanged subtrees pointing at exactly the same nodes as the prior snapshot. Both the old and new trees are valid; both are queryable; the new tree’s root is the only thing the system needs to know about to read it.
This is the same idea behind Clojure’s persistent vectors and Git’s object store. Datomic introduced it to databases in 2012; Datahike is the open-source descendant. Sharing is at the level of tree nodes: with a branching factor of 512, the tree stays shallow even for very large databases, and a transaction rewrites only the leaf and the few internal nodes on its path. Every other subtree is shared by pointer with the previous snapshot.
Each node is content-addressable — its key in konserve (the storage abstraction) is derived from its contents. konserve maps the same protocol over filesystems, S3, JDBC databases, IndexedDB in browsers, and others. A node written once is never rewritten. The only thing that ever changes is a small map at a well-known key listing the root pointers for the indices in the current snapshot. That map is a commit. A branch is a named pointer at a commit, registered in a :branches set under a known key.
The system reads the commit-id currently at :db, verifies it points at a real commit, writes a new key mapping :feature → <commit-id>, and updates the :branches set to include :feature. Two key writes in the simple case — plus a CoW-branch operation for any attached secondary index (Lucene full-text, vector indices) that implements the branching protocol.
Wall-clock time depends almost entirely on the storage backend:
In-memory — sub-millisecond.
Local filesystem — a few milliseconds, dominated by fsync.
S3 — 10–100 ms, dominated by the network round-trip; the payloads are tiny.
No tree nodes are copied. :feature and :db reach through the same physical objects in storage. A million-datom branch costs nothing extra at fork time, and a hundred branches are still a hundred small writes — not a hundred database copies.
If the source doesn’t exist, branch! raises :from-branch-does-not-point-to-existing-branch-or-commit. If the target name is already taken, it raises :branch-already-exists. Both are explicit; you don’t get silent overwrites.
Reading from a branch
Branches are first-class. You read them by name (branch-as-db), by commit-id (commit-as-db), or by holding a connection that was opened with a :branch in its config.
def feature-db: d/branch-as-db(conn :feature)
def main-db: d/branch-as-db(conn :db)
d/q('[:find ?e :where [?e :widget/sku]] feature-db)
;; Or pin to a specific historical commit by UUID
def older-db: d/commit-as-db(conn #uuid "b4f2e1c0-2feb-5b61-be14-5590b9e01e48")
(def feature-db (d/branch-as-db conn :feature))
(def main-db (d/branch-as-db conn :db))
(d/q '[:find ?e :where [?e :widget/sku]] feature-db)
;; Or pin to a specific historical commit by UUID
(def older-db (d/commit-as-db conn #uuid "b4f2e1c0-2feb-5b61-be14-5590b9e01e48"))
branch-as-db returns a database value — immutable, ready to query, safe to hold across calls. commit-as-db does the same for any historical commit, whether or not a branch still names it. Both work without an open connection on the target branch.
To write to a branch, connect with :branch in the config and transact normally:
The write goes to :feature’s head; :db is undisturbed. Each branch has its own writer; transactions on different branches don’t serialize against each other.
The commit graph
Every transaction produces a commit whose :meta :datahike/parents set records its parents. branch! produces single-parent commits (the previous head of the branch). merge! produces commits with multiple parents. Walking back from any commit gives you the lineage.
require('[superv.async :refer [<?? S]]
'[datahike.versioning :refer [branch-history]])
d/commit-id(@conn)
;; => #uuid "b4f2e1c0-…"
d/parent-commit-ids(@conn)
;; => #{#uuid "…"} ; single parent on a normal commit
;; => #{#uuid "…" "…"} ; two (or more) parents on a merge commit
<??(S branch-history(conn))
;; => sequence of stored DB values, in order from the current head back
;; through every ancestor reachable via :datahike/parents
(require '[superv.async :refer [<?? S]]
'[datahike.versioning :refer [branch-history]])
(d/commit-id @conn)
;; => #uuid "b4f2e1c0-…"
(d/parent-commit-ids @conn)
;; => #{#uuid "…"} ; single parent on a normal commit
;; => #{#uuid "…" "…"} ; two (or more) parents on a merge commit
(<?? S (branch-history conn))
;; => sequence of stored DB values, in order from the current head back
;; through every ancestor reachable via :datahike/parents
branch-history is the workhorse for inspection: it walks the parent graph from the connection’s current branch backward and returns each commit as a DB value, with duplicates pruned. Useful for time-travel reports, audit trails, and assembling queries against arbitrary historical states.
Merging: merge-db plus Datalog
This is where the “branches as values” property earns its keep.
merge-db records a new commit on the current branch whose :datahike/parents includes both the previous head and :feature’s head. The tx-data is regular transaction data; Datahike applies it the same way it applies any transaction. The operation is routed through the writer so it serializes cleanly against concurrent transactions on the same branch. (Sync; there’s also d/merge-db! for the async path, intended for go blocks and listener callbacks.)
What merge-db does not do: figure out the tx-data for you.
That’s a feature, not a gap. Because branches are database values and Datalog queries take multiple databases as inputs, the diff between branches is a query:
:in $feature $main binds two databases; :where clauses pick which one each pattern matches against. The result is the set of datoms present in :feature but absent in :db — directly transformable to tx-data.
Real merges are more selective. A few patterns that fall out naturally:
Filter by attribute — merge only the schema changes, leave the data behind:
Application-defined resolution — Datalog predicate clauses can call arbitrary functions, so routing each conflict through a domain resolver fits the same shape:
d/merge-db(conn
#{:feature}
mapv(fn [[e a v]]:
[:db/add e a v]
end diff-tuples))
(d/merge-db conn #{:feature}
(mapv (fn [[e a v]] [:db/add e a v]) diff-tuples))
branch-history then shows the merge commit; d/parent-commit-ids returns the full parent set.
The takeaway: Datahike doesn’t ship a built-in 3-way merge algorithm because it doesn’t need to. The merge algorithm is whatever Datalog query expresses your domain’s resolution rule. Three-way merge of textual files is hard because text has no semantics; merging datoms is a query because the data already carries its own structure.
This generalizes further than it looks. Martin Kleppmann has shown that CRDTs themselves can be expressed as pure Datalog queries over the operation log. Datahike’s merge model lets you adopt that approach incrementally: start with last-write-wins, add domain-specific resolvers where it matters, formalize as CRDT-shaped queries if you want full convergence guarantees.
Reset: force-branch!
force-branch! is the equivalent of git reset --hard. Pass a database value, a target branch, and the set of parent branches or commit-ids to attribute the new head to:
;; Rewind :feature to a known-good historical commit, treating it
;; as a fresh start from :db.
d/force-branch!(d/commit-as-db(conn #uuid "b4f2e1c0-…") :feature #{:db})
;; Rewind :feature to a known-good historical commit, treating it
;; as a fresh start from :db.
(d/force-branch! (d/commit-as-db conn #uuid "b4f2e1c0-…")
:feature
#{:db})
The branch head is overwritten unconditionally; the previous head becomes unreachable from the branch name. Existing connections to :feature are now stale and must be released and reconnected.
Useful for rolling back a bad branch after experimentation, pinning a branch to a known commit for audit, or rewriting a branch’s lineage when you need to. Use with care — the prior data isn’t deleted (GC controls that) but you’ve removed the named entry point, so if no other branch or commit-id references it, it goes away on the next sweep.
Removes :feature from the :branches set. The branch’s data stays in konserve, reachable by commit-id, until garbage collection sweeps it — that’s intentional, so you can recover a deleted branch if you change your mind. Live connections to :feature will fail after this; remote readers should release.
You can’t delete :db. It’s the default main branch and removing it would orphan the database; if you want the database gone, delete the database. Other branches are fair game.
require('[superv.async :refer [<?? S]])
;; Default: only reclaim space from deleted branches.
<??(S d/gc-storage(conn))
;; With a cutoff date: keep snapshots newer than the date plus all
;; branch heads; delete intermediate snapshots older than the date.
let [thirty-days-ago new java.util.Date(System/currentTimeMillis() - 30 * 24 * 60 * 60 * 1000)]:
<??(S d/gc-storage(conn thirty-days-ago))
end
(require '[superv.async :refer [<?? S]])
;; Default: only reclaim space from deleted branches.
(<?? S (d/gc-storage conn))
;; With a cutoff date: keep snapshots newer than the date plus all
;; branch heads; delete intermediate snapshots older than the date.
(let [thirty-days-ago (java.util.Date. (- (System/currentTimeMillis)
(* 30 24 60 60 1000)))]
(<?? S (d/gc-storage conn thirty-days-ago)))
Two things worth knowing about how gc-storage interacts with branch history:
Branch heads are always kept, regardless of cutoff. Every live branch’s current head survives every GC run; GC only removes the intermediate snapshots between commits — the dots between branch heads on the graph, not the latest dot on any branch.
Intermediate commits become unreachable below the cutoff. A 7-day cutoff means branch-history walks only return commits within that window plus the current heads, and d/commit-as-db lookups for older UUIDs fail because the snapshot is gone. The cutoff should also comfortably exceed your longest-running reader’s lifetime — Datahike’s distributed readers walk storage directly without coordinating with a writer, so a snapshot vanishing mid-query is a real failure mode. You’re trading old audit history (and reader safety) for disk space; pick the window to match your readers, compliance posture, and storage budget.
Without a date, d/gc-storage is always safe — it only reclaims storage from deleted branches. Datahike also ships an experimental online-GC mode that runs incrementally during transactions on single-branch databases; offline d/gc-storage is what you reach for in multi-branch setups.
A handful of workflows that branching makes affordable:
AI agent sandboxes. Spin up fifty branches, each agent gets its own database to mutate. Merge what works, drop the rest.
Schema migration tests in CI. Branch from prod, apply the migration, run the regression suite, throw the branch away. The next CI run starts from the same prod commit.
Editorial workflows. Editors stage changes on a branch, reviewers query the staging branch, approve, merge.
Multi-tenant snapshots. Each tenant gets a branch of a shared base. Tenant-specific overrides live on their branch; base updates merge cleanly.
Time-travel debugging. When a bug shows up, branch from the current head, apply experimental fixes on the branch, and walk historical commits via commit-as-db to find when the offending state appeared.
None of these require special infrastructure. The same primitives that read the database also read every branch.
Across the other bindings
The versioning API is part of the Clojure API spec, and the Java, JavaScript / TypeScript, Python (pydatahike), C (libdatahike), and CLI (dthk) bindings are all auto-generated from it. Java surfaces it as Datahike.branchAsync / branchAsDb / mergeDb; JavaScript as d.branchBang / branchAsDb / mergeDb; equivalent forms in the others. The dthk CLI also supports the more general Datalog-driven merge workflow via dthk query with multi-source input and dthk transact — see the CLI doc for an example.
In SQL via pg-datahike, the read side is wired through session variables and a datahike.* function namespace: SET datahike.branch = 'feature', SET datahike.commit_id = '<uuid>', plus datahike.branches(), datahike.create_branch(), datahike.delete_branch(). Write-side merge-db over SQL is on the roadmap. See Datahike Speaks Postgres for the full pgwire surface.
For how the same branching model extends beyond Datahike — to Stratum (SQL / columnar), vector and full-text indices, and other systems via a shared protocol — see Yggdrasil: Branching Protocols.
Known limitations
Multi-branch purge is expensive.purge removes datoms from the current branch; if you need them gone from every branch that referenced them (for GDPR or similar), the operation walks each branch independently. See Data Governance in Versioned Systems.
No built-in 3-way merge. Datahike doesn’t ship one because the right resolution rule is domain-specific. The Datalog patterns above cover the common shapes.
pg-datahike write-side merge-db is not yet exposed over SQL. Reads against any branch work; writes always land on the connection’s default branch in 0.1.
Branch-diff is O(differing datoms). The query walks both trees. For a 100M-datom database with a small diff, this is fast; for a diff that spans most of the tree, plan accordingly.
I keep seeing people share vibe-coded apps built on TypeScript/React + Supabase — seemingly the default recommendation from Lovable or Cursor. As a Clojure programmer, I can't stay quiet about this. In an era where AI agents are deeply embedded in the development workflow, that choice carries structural hidden costs that almost nobody is talking about.
Context Window Is the Bottleneck, and Framework Design Determines Burn Rate
LongCodeBench research shows that Claude 3.5 Sonnet's accuracy on bug-fixing tasks drops from 29% to 3% as context grows from 32K to 256K tokens. Chroma tested 18 frontier models and found the same pattern across all of them.
Coding agents accelerate this degradation: every tool call, every file read, every error message accumulates in the context. A 30-step agent session can consume more than ten times the context of a single conversation turn.
Countless efforts are already underway to manage context from the harness-design side — but the tech stack itself has an enormous impact on context efficiency that rarely gets discussed.
Task-Relevant Subgraph
An AI agent completing a task doesn't need to read the entire codebase — only the files relevant to that task. Call this set the task-relevant subgraph. The size of the subgraph is determined by the architectural design of the framework, not by the model.
The problem with TypeScript + React + Supabase is that a single feature naturally spans multiple layers — component, hook, state, API client, type definition — each living in a different file. The subgraph starts large and only grows as shared dependencies accumulate.
AI tends to recommend the stack it was trained on the most, but "easy to generate" is not the same as "efficient for long-term AI-assisted development." These are two different things.
What Makes a Stack More Agent-Ready
My current go-to is Clojure Stack Lite, and several of its design choices structurally shrink the task-relevant subgraph.
HTMX eliminates implicit client state. React state is scattered across multiple interdependent files; to verify behavior, an agent has to simulate browser interactions. HTMX is driven by server responses, so an agent can verify with a plain curl — the response is an HTML fragment, right or wrong, no ambiguity.
HoneySQL eliminates implicit lazy loading. When an ORM produces an N+1 problem, the debug subgraph includes model definitions, association configs, and migration files, because the issue is buried in implicit behavior. HoneySQL expresses queries as SQL-as-data — no lazy loading, no association magic. N+1 can't happen silently, because the syntax simply doesn't allow it to sneak in. The debug subgraph shrinks from five files to one.
Blocking IO eliminates implicit error paths. The fundamental problem with async isn't the syntax — it's that error paths are implicit. Every async call site is a potential break point where an exception can detach from the main flow. To locate a root cause, an agent must trace the entire call chain, and context width grows linearly with chain length. Clojure's blocking IO has no async boundaries; exceptions follow a single path — propagate upward, handled uniformly in middleware. When debugging, an agent only needs two places: the middleware log and the call site the log points to. Context scope stays fixed regardless of system size.
Explicit Over Implicit Is Not Just a Clojure Virtue
All three points share a common structure: the less implicit behavior, the smaller the context an agent needs to bring in.
The point here isn't a framework or language comparison — it's an observation about design philosophy. Explicit over implicit is a virtue for human developers; for AI agents, it's a structural guarantee that they won't go dumb prematurely.
Design principles the Clojure community has championed for years happen to be a competitive advantage in the AI agent era. I've chosen to frame this in terms of context efficiency, hoping it helps more people appreciate what the Clojure community figured out a long time ago.