A transcript is evidence, not memory

Every conversation your agents have is already recorded somewhere, in a format nobody will ever read again. Sessions takes those transcripts — with their threads, their branches and their turn order intact — keeps them searchable at message level, and promotes what matters into memory the retrieval engine can actually use.

  • 4 endpoints
  • 5,000 messages per call
  • Threads and branches preserved
  • Webhook on every ingest

Capture, summarise, promote

Three things have to happen to a transcript before it earns its storage. Doing only the first is an archive; doing all three is memory.

  1. Capture the transcript as it was

    Messages land in session_messages with their role, their timestamp, an estimated token count and a Postgres-generated search vector over the content. The session header keeps the title, the platform, the model and the start and end times. Re-ingesting the same session id updates it rather than duplicating it, and individual messages are inserted with a do-nothing conflict rule — so a client that replays the last twenty turns does not create twenty copies.

  2. Summarise the whole thing once

    On ingest the transcript is flattened to role-prefixed lines and summarised. With a provider configured, that is a model call. With no key at all, it falls back to a deterministic extractive summary — shorter and blunter, but produced without a network hop, a bill or a dependency. The summary is stored on the session row, and a later ingest will not overwrite a good summary with a null one.

  3. Promote it into durable memory

    The summary is written as a semantic memory titled with the session, with the session id in source_ids and a meta layer of session_summary, at confidence 0.7. From that moment it is an ordinary memory: embedded, indexed, retrievable, prunable, pinnable. The trust table rates a session summary at 0.58 authority — below a semantic memory you wrote yourself, well below a current fact — so it informs an answer without ever outranking one.

A session is stored in full and also condensed, and the condensed form is promoted into the same semantic layer as everything else — which is why a question can reach a conversation from six weeks ago without you naming it.

One endpoint takes the whole conversation

POST /v1/sessions/ingest accepts a session header and its messages in a single call. Nothing about it assumes the conversation happened here — the point is to absorb transcripts from wherever your agents actually run.

messages
One to five thousand per call. Each message takes up to 200,000 characters of content.
session_id
Supply your own and the call is an upsert: ended_at moves, meta merges, the summary is kept unless you send a better one.
source · platform · model
Where this transcript came from. Kept on the session so you can tell an imported thread from a live one.
thread_id · branch_id
Both default to main. Set them and a session stops being a flat list.
turn_index · parent_message_id
Explicit ordering and parentage. Omit turn_index and the array position is used.
summarize
On by default. Turn it off for bulk backfill and summarise later.
store_summary_memory
On by default. Writes the summary into semantic memory so ordinary retrieval can reach it.

Conversations are not lists

Real agent sessions fork. Somebody retries a prompt, a tool call spawns a sub-conversation, a branch gets abandoned halfway. Flattening that into one ordered array destroys the only information that made the transcript worth keeping.

Four fields carry the structure

thread_id separates parallel conversations inside one session. branch_id separates alternative continuations inside a thread. turn_index orders them. parent_message_id records what a message was replying to. Both identifiers default to main, so a flat transcript stays simple and a forked one stays truthful.

The index matches the question

Messages are indexed on tenant, user, session, thread and turn together — which is the exact shape of “give me turns 8 to 12 of the main thread”. Ordering falls back to created_at when a turn index is missing, so a partially annotated import still reads in the right order.

A matching line is rarely the answer

Search ranks messages with ts_rank_cd over a websearch_to_tsquery, and falls back to plain containment so a partial token still finds something. An empty query returns the most recent messages rather than an error.

The useful part is expand_adjacent. Set a window of up to ten and every hit brings back the turns either side of it, from the same session and the same thread, deduplicated across overlapping hits and labelled primary or adjacent. You get the exchange, not the fragment — which is the difference between “net-45” and “net-45 from April, but only on the retainer”.

The same search runs as GET or POST, because an MCP client and a browser want different things.

Reading a session back

GET /v1/sessions/{id}/timeline
The session header plus every event in turn order, each with its role, content, timestamp, thread and branch. Messages without a turn index sort last and fall back to their timestamp, so an incomplete import still reads sensibly instead of scrambling.
GET /v1/sessions/{id}/replay
The same events, plus a count and a flattened role: content transcript ready to paste into a prompt or a ticket. Replay is what you reach for when somebody asks what the agent was actually told before it did the thing it did.
session.ingested
Every successful ingest emits a webhook carrying the full session record — header, summary and normalised messages. A tracker, an indexer or a review queue can follow capture without polling.

Two things worth knowing before you wire it up

Session capture writes to Postgres when Postgres is the configured backend. Without it, sessions are appended to a local JSONL file — fine for a laptop or a first experiment, and not the thing to run a team on. Threads, branches, adjacency and search all work on both paths; only the storage differs.

Summarisation degrades rather than fails. No provider key means an extractive summary rather than a written one, and the resulting memory carries the same reduced confidence either way. Nothing about the capture path stops working because a model was unreachable.

Providers, fallbacks and bring-your-own keys

Give your agents a memory you can audit

Run the whole system on your own hardware under the MIT licence, or ask us about hosted access. Both start from the same place.