Ardian About Directory
Claude Code
Email me

Chronica

2026 — A Go library that lets one person and many AI agents share the memory of a session

01 Problem

An AI agent is a language model given a written instruction and a list of tools, functions the program runs when the model asks for them by name. The model remembers nothing between calls. All it knows about a conversation is the history the program hands it before each answer, and the text it reads before it answers is its context window, which only holds so much.

People remember what happened with episodic memory, the memory of events in the order they took place. An agent needs the same, a record of what was said and done in a conversation, called a session, that it reads back before every turn. That record holds more than messages. Before it answers, a model may write down a thought, ask for a tool, which is a tool request, and read what the tool returned, which is a tool response.

It gets harder when one person works with several agents in the same session, like a concierge that plans a dinner, a search agent that finds places, and a booking agent that holds the table. Each of them needs to see what the others did, and each needs only part of it. So every app that runs agents writes the same store again, the same way to cut the history down to what fits, and the same check that one person never reads the session of another.

My agents in Avagenc Chat, a group chat of AI agents I build at Avagenc, keep their sessions in Zep, a managed service. I started Chronica on 31 May 2026. Avagenc Chat's documentation, written on 25 July, counts what Zep costs and the time each call to it adds as known shortcomings, and names Chronica as the Go replacement being built for the session half of Zep.

02 Solution

go.trueardian.com/chronica is an open source Go library that keeps that record, a library being code other programs import rather than a program that runs on its own. It calls a session a chronicum and each thing that happened in it an actum, Latin for a thing done, and chronica and acta are simply their plurals.

Keeping a session takes three parts. A store keeps every chronicum and its acta. storeconformance, a set of tests, checks that a store keeps its promises. Chronicarius sits between the agents and the store, and it is the only part an app talks to. The next sections take them in that order, the store, storeconformance, and then Chronicarius.

Install
go get go.trueardian.com/chronica

03 Store

The store is where a session lives. For each chronicum it keeps an ID, the owner, who is the person the session belongs to, the moment it started, and the moment of its last actum. For each actum it keeps its own ID, the chronicum it belongs to, its kind, who did it, the content, and the moment it was written. The kind is a message, a thought, a tool request, or a tool response. Who did it takes two fields, an actor kind, which is human, agent, or system, and an actor, a stable ID like ardian or booking, since display names belong to a higher layer.

Chronica describes the store as an interface, a set of methods any type can provide, so a store can sit on any database. It has four methods. Create makes a chronicum, Record appends an actum to one, Acta reads its acta back, and Get reads the chronicum itself. None of them checks who is asking or whether an actum is well formed. Those rules live once in Chronicarius, so a plain store that only saves and reads rows is already a correct one.

It was not always that way. Until 1 June 2026, Chronicarius was itself the interface, and every backend had to create a session on its first write, check the owner, validate each actum, and keep the order on its own. That day the rules moved into one Chronicarius type, and the store shrank to four methods.

Chronica comes with two stores. The inmemory package keeps everything in one process, for tests and development. The postgres package, added on 7 June 2026, keeps each chronicum as a row in a table named chronica and each actum as a row in a table named acta. It is a Go module of its own, so the core imports nothing beyond Go's standard library, and only an app that uses Postgres pulls in pgx, the Postgres driver. WithAutoMigrate creates the tables at startup, under a lock that lets one instance run it while the others wait.

The Store interface
type Store interface {
Create(ctx context.Context, c Chronicum) error
Record(ctx context.Context, a Actum) (Actum, error)
Acta(ctx context.Context, chronicumID string, q ActaQuery) ([]Actum, error)
Get(ctx context.Context, chronicumID string) (Chronicum, error)
}
Create the Postgres store
store, err := postgres.NewStore(ctx, pool, postgres.WithAutoMigrate())

04 Store conformance

An interface only names the methods. It cannot say that Acta returns acta in the order they were written, or that two writes at the same moment both land, and a store that breaks either one still compiles. storeconformance turns those promises into tests that any store runs against itself.

RunTest checks the four methods in eleven cases, from the order of a thousand acta to forty written at once. RunIdempotentTest checks the retry safety a store may add on top, in three cases, among them forty writes at once that share one key. The in-memory store passes both, and the Postgres store runs both when it is given a database.

The tests began on 1 June 2026 as chronicatest, the same day the store shrank to four methods, and were renamed storeconformance on 7 June. That day they also began to require that Acta fail with ErrChronicumNotFound, a named error, for a chronicum that does not exist. A SQL store that finds no rows naturally answers with an empty list and no error, and until then such a store had passed.

Run the suites against a store
func TestConformance(t *testing.T) {
storeconformance.RunTest(t, inmemory.NewStore)
storeconformance.RunIdempotentTest(t, inmemory.NewIdempotentStore)
}

05 Chronicarius

Chronicarius is the part an app talks to. NewChronicarius takes a store, and from there an app has three calls. RecordActum writes an actum, GetActa reads back the acta an agent needs, and GetChronicum reads the session itself. Every call names the owner, so a session belongs to someone from its first actum.

RecordActum checks the actum before the store is touched. It needs a chronicum, an actor, and a kind and an actor kind it knows. A message or a thought needs content, while a tool request or a tool response may be empty, because a call with no arguments and a call that returns nothing are both real, and that has been allowed since 7 June 2026. Chronicarius then gives the actum its ID, replacing any the caller sent, and when the chronicum does not exist yet, it creates it on the spot for that owner.

It took this shape within a week. It began on 31 May 2026 as go.naturallyfunny.dev/chronica, with an interface named Chronicarius whose first call, AddActum, became RecordActum the same day. It became one type on 1 June, New became NewChronicarius on 2 June, and on 5 October the module moved to go.trueardian.com.

Each call then follows the same few rules. It first checks whose chronicum it is, GetActa cuts the history down to a context window and returns it in order, and RecordActum can be retried without writing twice. The next four sections take them one at a time.

Packages
inmemory         ──┐
storeconformance ──┼─▶ chronica
postgres ──┘
Give Chronicarius a store and record an actum
c := chronica.NewChronicarius(store)

stored, err := c.RecordActum(ctx, "ardian", chronica.Actum{
ChronicumID: "dinner-friday",
Kind: chronica.ActumMessage,
ActorKind: chronica.ActorHuman,
Actor: "ardian",
Content: "Find us a quiet table for four in Kemang on Friday at 7pm.",
})

06 Ownership

A chronicum belongs to one owner, set by its first RecordActum. Every later call names an owner too, and Chronicarius compares it with the chronicum's before any acta are read or written. A chronicum that belongs to someone else answers ErrChronicumNotFound, the same as one that does not exist, so nobody can learn which IDs other people use. A call with an empty owner fails with ErrEmptyOwnerID.

The store methods take no owner and check nothing, so every read and write has to go through Chronicarius. The owner is a plain string from whatever sign-in the app uses, so proving who the caller is stays with the app.

On 2 June 2026 GetChronicum briefly became a plain lookup by ID, which let anyone holding an ID read whose session it was and when it started, and the same day the owner check was put back. Also on 2 June, when an owner made two first writes into the same new chronicum at once, the slower one found the chronicum already made, answered ErrChronicumNotFound, and its actum was lost. It now checks the owner again and records the actum.

07 Context window

An agent rarely needs a whole session. A reply needs the last few messages, not every thought before them, and a model can only read so much. GetActa takes three options. WithActumKinds keeps only some kinds, WithActorKinds keeps only some actor kinds, and WithLastN keeps the most recent of what is left. A limit of zero or less means no limit, and leaving a filter out lets every kind through.

The filters run before the limit, never after it. So the last three messages are always three messages, even when thoughts and tool calls were written between them.

WithActorKinds was added on 1 June 2026. On 2 June a negative limit began to mean no limit, the same as zero, and since 7 June the last WithLastN wins, so WithLastN(0) clears a limit set before it.

The Postgres store does it in one query, which takes the newest rows that pass the filters, up to the limit, and turns them back to oldest first.

  1. Your code using this library sets up c := chronica.NewChronicarius(inmemory.NewStore())
  2. Your code using this library calls c.GetActa(ctx, "ardian", "dinner-friday", chronica.WithLastN(3), chronica.WithActumKinds(chronica.ActumMessage))
  3. Then this library checked that dinner-friday belongs to ardian (OwnerID "ardian")
  4. Then this library left out the thought and the tool call, keeping the messages (4 of 7 acta)
  5. Then this library kept the last 3 of those, oldest first (search, ardian, concierge)
  6. The code results in the places found, the one ardian chose, and the request to book it ([]chronica.Actum, 3 acta, err nil)
  1. Your code using this library calls c.GetActa(ctx, "rafa", "dinner-friday")
  2. Then this library looked up dinner-friday (OwnerID "ardian")
  3. Then this library saw it belongs to someone else and answered as if it did not exist
  4. The code results in the same error a session that was never used returns (chronica.ErrChronicumNotFound)
  1. Your code using this library sets up reply.Content = "All set. A table for four at Rumah Teduh on Friday at 7pm, confirmation RT-4821."
  2. Your code using this library calls c.RecordActum(ctx, "ardian", reply, chronica.WithIdempotencyKey("booking-reply-1"))
  3. Then this library checked the reply and that dinner-friday belongs to ardian (OwnerID "ardian")
  4. Then this library found booking-reply-1 already used in dinner-friday (ID 4c450f10d540ac098913413c6c830025)
  5. Then this library discarded the reworded reply and wrote nothing (8 acta, as before)
  6. The code results in the reply stored the first time (stored.ID "4c450f10d540ac098913413c6c830025", err nil)
The booking agent's context window
acta, err := c.GetActa(ctx, "ardian", "dinner-friday",
chronica.WithLastN(3),
chronica.WithActumKinds(chronica.ActumMessage),
)
Seven acta in, three out
  message        ardian     Find us a quiet table for four in Kemang on Friday at 7pm.
thought concierge Search first, then hand the booking over.
tool_request search {"area":"Kemang","party":4,"quiet":true}
tool_response search [{"name":"Rumah Teduh","table":true},{"name":"Kayu Manis","table":true}]
▶ message search Rumah Teduh and Kayu Manis both have a quiet table for four at 7pm.
▶ message ardian Rumah Teduh.
▶ message concierge Book Rumah Teduh for four at 7pm on Friday under Ardian.

08 Ordering

A model reads its context from top to bottom as what happened first and what happened next, so acta that come back shuffled tell it a different story. GetActa returns them in the order RecordActum was called, oldest first.

Every actum also carries At, the moment the store wrote it, but At is only advisory and never decides the order, so two acta written in the same instant still come back the way they went in.

On 1 June 2026 the rule first asked every backend to sort by At and break ties by ID, and before the day was over the order became the order of writing itself. From then until 7 June an actum could also carry OccurredAt, the time the caller said it happened, which never affected the order either, and it was removed.

The in-memory store appends each actum to a list. The Postgres store numbers every row with seq, a counter Postgres raises on each insert, and sorts by it.

09 Idempotency

When a write times out, the agent cannot tell whether its reply landed, and writing it again may record it twice, so the next context window shows the same reply twice. Idempotency means that doing something twice has the same effect as doing it once.

WithIdempotencyKey gives RecordActum a key. A retry with the same key in the same chronicum returns the actum stored the first time and discards the new one, so a reply the model wrote again in other words is still recorded once. Many calls at once with one key store exactly one actum.

Not every database can do that cheaply, so a store opts in by also implementing IdempotentStore, which adds RecordIdempotent. Given a key and a store that does not, RecordActum fails with ErrIdempotencyUnsupported before it creates anything, because quietly ignoring the key is how duplicates get in.

On 1 June 2026 the key was a field on every actum, and every store had to deduplicate. On 7 June it became an option on the call and a capability a store can choose to add.

Both stores add it. The in-memory store locks the chronicum while it looks up the key and appends, and the Postgres store keeps a unique index on the chronicum and the key, so an insert that hits it does nothing and the actum stored the first time is read back.

Retry a reply
stored, err := c.RecordActum(ctx, "ardian", reply,
chronica.WithIdempotencyKey("booking-reply-1"),
)

10 Error handling

Chronica names five errors, which an app matches with errors.Is, Go's way of asking whether an error is a known one, even when it is wrapped inside another. ErrChronicumNotFound is a chronicum that does not exist or is not the caller's, and ErrChronicumExists is a Create for an ID already taken. ErrInvalidActum is an actum RecordActum refused, followed by what was wrong, like Content is empty. ErrEmptyOwnerID is a call with no owner, and ErrIdempotencyUnsupported is a key given to a store that cannot use it.

Every other error, like a lost database connection, comes back from the store as it is, and the Postgres store puts where it was raised in front of it, like postgres: record actum.

The one place Chronica panics is the ID, when the random source of the system fails, because going on without it would make IDs that collide or can be guessed.

Answering a request
acta, err := c.GetActa(ctx, owner, id)
if errors.Is(err, chronica.ErrChronicumNotFound) {
w.WriteHeader(http.StatusNotFound)
}

stored, err := c.RecordActum(ctx, owner, actum)
if errors.Is(err, chronica.ErrInvalidActum) {
w.WriteHeader(http.StatusBadRequest)
}

11 Limits

It has no delete and no pagination. A chronicum is an append-only record of what the agents and their person did, and nothing in the agent layer needs to erase an actum. Pages and cursors drive scrolling lists in an interface, while agents assemble context windows. Until 1 June 2026 it could also list the sessions of an owner a page at a time, and that day the listing was removed, so an app that needs either builds it on top.

It is not access control. The owner check keeps an honest app from mixing up sessions, but the owner is whatever string the app passes, and the store answers anyone who holds it.

The in-memory store is for tests and development. It keeps everything in one process, its idempotency keys grow without bound, and it was written entirely by Claude, as the README says.

Nothing uses it yet. Avagenc Chat still keeps its sessions in Zep.

Chronica

Any Go app keeps one session for a person and every agent they work with, and hands each agent the part it needs in one call, in the order it happened and never to anyone else