DEV Community

Shrijith Venkatramana
Shrijith Venkatramana

Posted on AI-assisted

How chDB Turns Ephemeral Agent State into Durable Memory

Hello, I'm Shrijith Venkatramana, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. Star us to help devs discover the project, give it a try, and share your feedback to help improve the product.


Agent memory sounds like an LLM problem until you actually deploy an agent.

A useful agent needs to retain things such as:

project conventions
previous decisions
tool results
failed approaches
user preferences
investigation history

Now put that agent inside a temporary CI runner or Lambda or sandbox.

Where does all of that state live?

A file works well until you need the agent to move to another machine. A remote database solves portability, but introduces a network dependency for every read and write.

chDB Durable occupies the space between the two.

It keeps the database embedded and local, while using object storage as the durable backing layer.

This becomes common as agents move between laptops, CI runners, sandboxes, containers, and short-lived cloud machines. A local agent can accumulate weeks of useful state: project conventions, user preferences, tool failures, corrected assumptions, previous decisions, and the evidence behind those decisions.

A SQLite file can preserve some of this.

A remote Postgres or ClickHouse server can preserve much more.

But there is a useful middle ground:

Keep the database local for fast reads, while making its state durable in object storage.

That is the idea behind the new chDB Durable Layer.

1. Agent memory is becoming a database problem

A useful way to think about an agent is as a loop:

observe
  -> reason
  -> act
  -> observe
  -> reason
  -> act
  -> ...
Enter fullscreen mode Exit fullscreen mode

Memory sits across this loop.

The simplest implementation is often a list of messages:

messages = [
    "User prefers uv over Poetry",
    "The checkout service runs in eu-west-1",
    "Do not modify migrations during deployment",
]
Enter fullscreen mode Exit fullscreen mode

Eventually that becomes insufficient.

Suppose an agent originally learns:

Deploy checkout-api to eu-west-1.
Enter fullscreen mode Exit fullscreen mode

Later, someone changes the infrastructure:

Deploy checkout-api to eu-central-1.
Enter fullscreen mode Exit fullscreen mode

What should the agent store?

Just the new answer?

Or also:

old belief
new belief
who changed it
when it changed
why it changed
what evidence supported it
which agent used the old belief
Enter fullscreen mode Exit fullscreen mode

This is where the problem starts looking much more like a database than a prompt.

The research literature has been moving in this direction for several years. MemGPT, for example, explicitly treats an LLM agent as a system with multiple memory tiers and a mechanism for moving information between them. The underlying idea is familiar from operating systems: fast working memory is valuable, but larger persistent state must exist somewhere else.

Agent memory is therefore partly a data placement problem.

The interesting question becomes:

Where should the agent's durable state live?

2. The SQLite-to-server spectrum has a missing middle

There are several obvious answers.

Local SQLite

For a small agent, SQLite is excellent.

It is embedded, transactional, mature, and requires no server.

An agent can checkpoint its state:

agent process
    |
    v
SQLite file
Enter fullscreen mode Exit fullscreen mode

The problem appears when memory becomes historical and analytical.

You start wanting queries such as:

SELECT project, count()
FROM tool_events
GROUP BY project;
Enter fullscreen mode Exit fullscreen mode

Or:

SELECT *
FROM memory_history
WHERE memory_id = 'deployment-region'
ORDER BY version;
Enter fullscreen mode Exit fullscreen mode

Or:

SELECT model, sum(tokens), sum(cost)
FROM events
WHERE created_at >= today() - 7
GROUP BY model;
Enter fullscreen mode Exit fullscreen mode

That is a different workload from "save my current checkpoint."

SQLite plus replication

You can replicate SQLite's WAL to object storage.

That solves the single-disk durability problem.

But now there is another component in the deployment:

agent
  |
SQLite
  |
replication process
  |
object storage
Enter fullscreen mode Exit fullscreen mode

There is more operational machinery, and the underlying database is still fundamentally a transactional row store.

Remote database

The other solution is to put memory into Postgres, pgvector, ClickHouse, or a managed memory platform.

This solves portability and shared access.

The architecture becomes:

agent
   |
network
   |
database server
Enter fullscreen mode Exit fullscreen mode

Now every recall is potentially a remote operation.

For a conventional application, this is often perfectly reasonable.

For an agent, the distinction matters because one user request can generate many sequential tool calls.

If an agent makes 10 data-access operations and each introduces 50 ms of network latency:

10 * 50 ms = 500 ms
Enter fullscreen mode Exit fullscreen mode

Half a second appears before counting model inference.

And that is the optimistic case. Retries, connection failures, authentication, network jitter, and service saturation add another dimension.

Embedded analytical database

Now consider chDB.

It puts the ClickHouse query engine inside the application process.

agent process
    |
   chDB
    |
local database
Enter fullscreen mode Exit fullscreen mode

This gives the agent SQL, columnar storage, analytical queries, and local execution without requiring a database server.

But one problem remains:

The database is still attached to the machine.

This was the problem the chDB Durable Layer was designed to address.

3. The idea: local working copy, durable object

The architecture is simple enough to draw on a whiteboard:

                 object storage
              authoritative state
                     |
             flush / checkpoint
                     |
                     v
+---------------------------------------+
|              Agent process            |
|                                       |
|      chDB + local MergeTree           |
|                                       |
|      recall -> local SQL query        |
|      writes -> local WAL buffer       |
|                                       |
+---------------------------------------+
Enter fullscreen mode Exit fullscreen mode

The crucial distinction is between the working copy and the authoritative copy.

The agent queries its local chDB database.

Object storage holds the durable state.

When the agent needs to publish changes, it calls:

obj.flush()
Enter fullscreen mode Exit fullscreen mode

When it wants to create a new base snapshot and shorten future recovery:

obj.checkpoint()
Enter fullscreen mode Exit fullscreen mode

The result is an interesting deployment shape:

No database server
No persistent volume requirement
No replication sidecar
No network call for every recall
Enter fullscreen mode Exit fullscreen mode

The durable object has an identity.

For example:

s3://my-agent-state/agent-memory
└── acme-checkout-api
Enter fullscreen mode Exit fullscreen mode

The namespace is the storage root.

The object is one complete chDB database.

That object can later be opened on another machine.

This is particularly useful for agents because their compute environment is increasingly disposable.

Imagine:

Laptop
   |
   | develop
   v
CI runner
   |
   | test
   v
sandbox
   |
   | reproduce bug
   v
another machine
Enter fullscreen mode Exit fullscreen mode

The runtime can disappear.

The agent's accumulated state does not have to.

4. What actually happens during durability?

The interesting engineering is underneath the small API.

There are four concepts to keep in your head.

execute(): mutate locally

An operation such as:

obj.execute(
    "INSERT INTO memories VALUES (...)"
)
Enter fullscreen mode Exit fullscreen mode

changes the local working database.

The mutation enters the WAL buffer.

It has not necessarily become durable yet.

flush(): make a promise real

obj.flush()
Enter fullscreen mode Exit fullscreen mode

is the durability boundary.

When it returns, the writes covered by that flush have reached object storage.

That gives application code an important semantic rule:

execute()
    = "I changed my local memory."

flush()
    = "I promise this memory survived the machine."
Enter fullscreen mode Exit fullscreen mode

For an agent API, this distinction matters.

Consider a tool:

remember("The deployment region is eu-central-1")
Enter fullscreen mode Exit fullscreen mode

Returning success immediately after execute() means the tool may claim to have remembered something that disappears when the sandbox disappears.

Returning success only after flush() gives the tool an actual durability guarantee.

checkpoint(): create a new base

Between checkpoints, the durable state consists conceptually of:

base snapshot
+
WAL segments
Enter fullscreen mode Exit fullscreen mode

Recovery therefore looks like:

restore base
+
replay committed WAL
=
current database
Enter fullscreen mode Exit fullscreen mode

As the WAL grows, recovery has more work to do.

A checkpoint folds the current state into a new base.

So there is a tradeoff:

frequent checkpoint
    -> more transfer work
    -> shorter recovery

infrequent checkpoint
    -> less transfer work
    -> more WAL replay
Enter fullscreen mode Exit fullscreen mode

This is a familiar database systems tradeoff appearing inside an agent architecture.

head.json: who owns the database?

There is another problem.

Suppose two workers open:

acme-checkout-api
Enter fullscreen mode Exit fullscreen mode

at the same time.

Both could believe:

"I am the writer."
Enter fullscreen mode Exit fullscreen mode

That is dangerous.

Durable uses a head record with conditional writes to establish lease and fencing semantics.

At a high level:

read current head = version 17

writer A:
    CAS(17 -> 18)  succeeds

writer B:
    CAS(17 -> 18)  fails
Enter fullscreen mode Exit fullscreen mode

Only one process becomes the current writer.

This gives Durable a deliberate constraint:

one writer per object.

That is a useful property for a per-user or per-project agent brain.

It is a poor fit for a team database in which ten independent processes continuously update the same object.

5. Store memory as history, not as one giant document

This is where using an analytical database becomes interesting.

A naive memory system might have:

memory.json
Enter fullscreen mode Exit fullscreen mode

A more useful design has several relations:

memories
memory_history
raw_transcripts
events
Enter fullscreen mode Exit fullscreen mode

For example:

CREATE TABLE memory_history
(
    memory_id String,
    version UInt32,
    op LowCardinality(String),
    content String CODEC(ZSTD(3)),
    edited_by String,
    edited_at DateTime64(3, 'UTC'),
    prev_id String,
    note String
)
ENGINE = MergeTree
ORDER BY (memory_id, version, edited_at);
Enter fullscreen mode Exit fullscreen mode

Now a revision does not destroy the original fact.

Instead:

version 1:
Deploy checkout-api to eu-west-1.

version 2:
Deploy checkout-api to eu-central-1.
Enter fullscreen mode Exit fullscreen mode

You can query the current state.

You can also query the history.

You can even ask:

Why did the agent believe this?

Which evidence produced the belief?

When did the belief change?

How often was this memory recalled?

Did those recalls actually help?
Enter fullscreen mode Exit fullscreen mode

That last class of questions is important.

A memory system should eventually become capable of analyzing its own memory.

That is naturally expressed with SQL.

Raw evidence and actual memory are different

One especially useful distinction in the ClickMem design is between evidence and memory.

A transcript may contain:

User:
"Maybe the service is in eu-west-1?"

Tool:
"Deployment failed."

Agent:
"Let's inspect the infrastructure."
Enter fullscreen mode Exit fullscreen mode

None of that necessarily deserves promotion into long-term memory.

Instead:

raw_transcripts
    = searchable evidence

memories
    = curated beliefs
Enter fullscreen mode Exit fullscreen mode

This reduces a common failure mode of memory systems:

every conversation
    -> memory
    -> future behavior
Enter fullscreen mode Exit fullscreen mode

A transient mistake then becomes persistent behavior.

The alternative is:

conversation
    -> evidence
    -> evaluation
    -> memory
Enter fullscreen mode Exit fullscreen mode

That resembles the observation/retrieval/reflection pattern explored in early generative-agent research, but gives the resulting state a conventional data model.

6. The math, storage economics, and operational tradeoffs

There are two pieces of mathematics worth understanding.

Semantic recall

Suppose every memory has an embedding vector m, and the current task has query vector q.

A common semantic score is cosine similarity:

score(q, m) = (q dot m) / (||q|| ||m||)
Enter fullscreen mode Exit fullscreen mode

The intuition is simple:

same direction
    -> high similarity

different direction
    -> low similarity
Enter fullscreen mode Exit fullscreen mode

A memory query can therefore look like:

1. Filter project
2. Filter active memories
3. Generate semantic candidates
4. Rank candidates
5. Return top K
Enter fullscreen mode Exit fullscreen mode

For a brute-force scan with:

N memories
D embedding dimensions
Enter fullscreen mode Exit fullscreen mode

the rough amount of dot-product work is:

N * D
Enter fullscreen mode Exit fullscreen mode

For example:

N = 100,000 memories
D = 1,536

N * D
= 153,600,000
Enter fullscreen mode Exit fullscreen mode

That is roughly 154 million multiply-add terms for a full scan.

Whether that is acceptable depends on hardware, query frequency, and filtering. The useful observation is that memory retrieval is a data-processing problem, so the choice between prefiltering, vector indexes, approximate nearest-neighbor methods, and brute-force scans becomes a database engineering decision.

chDB already provides the local analytical machinery around the embedding column; the durability layer does not need to know whether your recall logic is lexical, vector-based, temporal, or some combination.

Storage is often cheaper than the memory architecture makes it look

ClickHouse published an instructive experiment using a 1.45 GB Claude Code transcript sample containing 213,721 rows.

Loaded into MergeTree:

ZSTD(3): 521 MiB
LZ4:     991 MiB
Enter fullscreen mode Exit fullscreen mode

So the ZSTD representation is roughly:

521 / 1,383 ~= 0.38
Enter fullscreen mode Exit fullscreen mode

or about a 62% reduction relative to the original binary size.

There was another useful observation:

The screenshots represented only about 0.8% of rows, but roughly two-thirds of the bytes.

That leads to a practical rule:

textual history -> compress in the database

large binary objects
    -> keep outside the table
    -> store object keys in the database
Enter fullscreen mode Exit fullscreen mode

Otherwise your screenshots, audio, or video become part of every checkpoint.

A rough storage-cost calculation

Suppose an agent eventually accumulates:

0.5 GiB compressed durable state
Enter fullscreen mode Exit fullscreen mode

Using an illustrative S3 Standard rate of:

$0.023 / GB-month
Enter fullscreen mode Exit fullscreen mode

the raw storage component is approximately:

0.5 * 0.023
= $0.0115/month
Enter fullscreen mode Exit fullscreen mode

about one cent per month.

That is not a complete cloud bill. PUT/GET requests, transfer, replication, and the exact storage class and region all matter.

But the calculation exposes something useful:

For small per-agent memory stores, storage bytes are unlikely to be the expensive part.

The architectural costs are more likely to come from:

inference
network calls
database operations
operational complexity
recovery behavior
Enter fullscreen mode Exit fullscreen mode

This is why a one-bucket architecture can be attractive.

Instead of paying continuously for:

database server
connection pool
database operations
persistent volumes
replication sidecars
Enter fullscreen mode Exit fullscreen mode

you retain a local process and pay for durable state only at explicit boundaries.

The tradeoff is that you now own the lifecycle:

IAM
bucket policies
encryption
object naming
retention
flush policy
checkpoint policy
Enter fullscreen mode Exit fullscreen mode

That is a much smaller operational surface than operating a database cluster, but it is still an engineering responsibility.

7. When should developers use Durable?

The cleanest decision rule is based on the shape of the workload.

Use plain local chDB when:

the state is disposable
or
the state never leaves one machine
Enter fullscreen mode Exit fullscreen mode

Use a transactional store when:

you mainly need checkpoints
and
you do little historical analysis
Enter fullscreen mode Exit fullscreen mode

Use a server database when:

many writers
many clients
centralized governance
large working sets
continuous shared access
Enter fullscreen mode Exit fullscreen mode

The Durable Layer fits the middle:

one logical owner
+
local analytical queries
+
append-heavy history
+
portable durable state
Enter fullscreen mode Exit fullscreen mode

A useful deployment might look like:

                   S3 / GCS / Azure
                         |
                 durable agent object
                         |
          +--------------+--------------+
          |                             |
       laptop                         CI runner
          |                             |
       chDB                          chDB
          |                             |
      local SQL                     local SQL
          |                             |
       recall                         recall
Enter fullscreen mode Exit fullscreen mode

The database moves.

The query engine stays embedded.

The service layer disappears.

And the boundary between "memory" and "data engineering" becomes much thinner.

There is also a nice historical symmetry here.

ClickHouse itself began at Yandex under Alexey Milovidov and colleagues as an attempt to generate real-time analytical reports from continuously arriving, non-aggregated data. Its early production system eventually became the engine behind Yandex.Metrica.

Years later, the same broad philosophy is useful for agents:

keep the raw history
store it efficiently
query it later
derive answers from the history
Enter fullscreen mode Exit fullscreen mode

The difference is that the data is now an agent's experiences rather than web analytics.

That may be one of the more useful ways to think about agent memory.

Memory is a database that happens to influence future decisions.

And once you view it that way, persistence, provenance, revision history, compression, query latency, leases, checkpoints, and recovery stop looking like infrastructure details. They become part of the agent's cognitive architecture.

The chDB Durable Layer is interesting because it gives developers a missing deployment primitive:

embedded analytical state
+
portable durability
+
object storage
+
no database server
Enter fullscreen mode Exit fullscreen mode

The result is a local agent brain that can survive the machine that created it.

What do you think the right primitive for long-lived agent memory should be: a vector store, a transactional database, or an analytical database with durable local state?



Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production reliable and secure without slowing you down.

I'm building LiveReview, a blast-radius aware AI code review built for your business-critical systems.

Instead of presenting every diff with equal emphasis, LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.

Spend code review effort where business risk is highest — not spread evenly across every diff.

⭐ Star it on GitHub:

GitHub logo HexmosTech / LiveReview

Blast-Radius Aware AI Code Review for Business-Critical Systems

LiveReview

gitleaks.yml osv-scanner.yml govulncheck.yml semgrep.yml dependabot-enabled mcp-testcases.yml

LiveReview: Blast-Radius Aware AI Code Review for Business-Critical Systems

LiveReview is an AI code reviewer that scores every hunk of a diff by blast radius: how far a change reaches through your call graph, how much persistent state it touches, and how well-tested it is. A 3-line change to a shared auth check can outrank a 300-line UI tweak. Your team's attention goes to the highest-risk code first, not spread evenly across every diff.

blast-radius-demo.mp4

LiveReview's Blast Radius & Review Priority scoring, live in the diff viewer.
















The exact math, not a black box Visualize blast radius at a glance Every factor that feeds the score

How does Blast Radius scoring work? (a more technical explanation)

Here's the goal:

  • A 3-line fix in a function used by 40 other files, that also writes to a database, should score high.
  • A 300-line UI change in one file, fully covered by…




Click below to try LiveReview with your codebase:

LiveReview Banner

Top comments (0)