Technology10 min read

We built three memory systems. The most expressive one wasn’t the most useful.

By · Published by Everything Blog

In short

### Briefing We improved Eon, the Slack agent used across Tiger Data, by building three agentic memory systems: a list of user-scoped facts, a general knowledge graph, and a tree-based system. As we watched agents use them, we learned that a more expressive system was not necessarily more effective. Other teams began using our tree-based system for applications beyond agent memory, extracting the reusable search infrastructure into searchgres.js, an open-source TypeScript library for hybrid search with composable…

Key points

  • ### Briefing We improved Eon, the Slack agent used across Tiger Data, by building three…: ### Briefing We improved Eon, the Slack agent used across Tiger Data, by building three agentic memory systems: a list of user-scoped facts, a general knowledge graph, and a tree-based system.
  • As we watched agents use them, we learned that a more expressive system was not necessari…: As we watched agents use them, we learned that a more expressive system was not necessarily more effective.
  • Other teams began using our tree-based system for applications beyond agent memory, extra…: Other teams began using our tree-based system for applications beyond agent memory, extracting the reusable search infrastructure into searchgres.js, an open-source TypeScript library for hybrid search with composable filters in PostgreSQL.

Sources checked by our analysis pipeline: 1

Introducing searchgres.js: Composable Search in PostgreSQL

searchgres.js composes BM25, semantic ranking, and filters for hierarchy, metadata, and time in a single Postgres query. Open-source TypeScript, Apache-2.0.

By John Pruitt

October 20th, 2026

By John Pruitt

October 22nd, 2026

7 min

As we improved Eon, the Slack agent used across Tiger Data, we granted it access to more data and gave it memory. Both presented the same challenge: getting the right information into context while excluding irrelevant information. It was a search problem.

For memory, we wanted agents to choose what to remember, how to organize it, and what to retrieve. Along the way, we built three agentic memory systems: a list of user-scoped facts, a general knowledge graph, and a tree-based system. As we watched agents use them, we learned that a more expressive system was not necessarily more effective.

Other teams began using our tree-based system for applications beyond agent memory. They wanted its search capabilities, but already had their own users, permissions, and workflows that didn’t align with our memory system. We extracted the reusable search infrastructure into searchgres.js, an open-source TypeScript library for hybrid search with composable filters in PostgreSQL.

Our first memory system was deliberately small. We built tiger-memory-mcp-server

, which gave Eon four operations: remember

, recall

, update

, and forget

.

Memories were scoped to the user, and each memory was just a snippet of text. There was no structured way to relate memories or annotate them with context that could help narrow retrieval. recall

returned the full list of the user’s memories, whether relevant or not.

This worked for personal preferences, but not as well for knowledge about our work. We wanted project knowledge to be available across users, rather than confined to one person’s memory list.

Capture was limited, too. Either the user had to tell Eon what to remember, or Eon had to decide to call the memory tool during a conversation. We had no proactive process for capturing useful information.

We had given Eon somewhere to store memories without doing much to help it decide what to save or retrieve. Only a handful of power users took advantage of it.

For the second attempt, we chose something much more ambitious. We wanted to represent memories more expressively than a list of facts. The agentic-memory debate included knowledge graphs, vector databases, metadata stores, temporal models, and plain filesystems. We realized that PostgreSQL gave us all the primitives to experiment with any of these techniques.

We chose a general knowledge graph with metadata, time, and full-text search. It used two PostgreSQL tables, node

and edge

. Nodes stored content, JSONB metadata, and an optional event time or validity period. Edges connected the nodes and carried metadata describing each relationship. pg_textsearch provided BM25 keyword search over node content.

We deployed the graph-based system internally and connected it to Eon, though we never released it publicly. At the time, many of Eon’s answers came from searching Slack, and our initial use case was Slack summarization, with plans to expand to other sources and workflows.

Scheduled agents identified active Slack threads and channels, then delegated thread, daily, and weekly summaries to separate agents. Each summarization task yielded a memory in the node

table. Memory no longer depended on a user asking Eon to remember something.

The edge

table’s role was less clear. We could connect thread summaries to daily summaries and daily summaries to weekly ones, but we had not established which relationships would improve retrieval. Agents had to decide what links meant, which to create, and when to follow them.

This produced inconsistent results; edges used different metadata keys and values for the same attributes. Sometimes edges were missing altogether. So, we tried defining the relationship semantics up front and specifying them in the prompts. While we gained more consistency, we still had to make assumptions about what might be effective.

We had chosen flexibility before we knew which structures would be useful.

We could have defined a set of tools for manipulating nodes and edges, but it was unclear which operations would be most useful, and we wanted agents to be able to experiment freely. So, we also gave agents arbitrary SQL access, with detailed Agent Skills that described the schema. SQL enabled them to express any desired action.

The agents became surprisingly good at SQL. But alongside deciding what knowledge to store and retrieve, they now had to understand the schema, generate correct database operations, and recover from failures. Agents spent time and tokens on database mechanics rather than the memory task itself. We observed agents taking dozens of turns to author and execute queries to integrate a new thread summary into memory.

For the next attempt, we wanted to preserve room for experimentation without making agents spend so much effort deciding how to organize data and operate the database.

The idea of progressive disclosure in Agent Skills and Letta’s experiments with filesystem tools for agent memory inspired a simplification. Instead of a graph, we could use a tree to organize memories. A tree, a constrained form of graph, would give each memory a place, and its path would make that place visible. Both humans and agents intuitively understand how to navigate folders in a filesystem; we could adopt the same scheme.

PostgreSQL’s ltree extension lets us represent that hierarchy directly. A path such as share.projects.my_coding_project

places a record in a tree. A GiST index supports queries over subtrees, ancestor paths, and path patterns.

We replaced the two-table graph with one memory table. When each memory had an ltree path, the tools the agent needed became obvious. We gave agents fewer ways to model memories, but a more effective way to work with them. Agents still chose what to store, where to store it, and how to search for it.

Specifying the ltree path on creation established a memory’s place in the tree. The agent didn’t need to insert and update edge records to connect it to a graph. Searching under a path was a simple ltree operation; no need for recursive CTEs. Operations like moving, copying, or deleting memories by path became straightforward tool calls rather than complex SQL. Integrating a new thread summary had taken agents dozens of turns. Now, it usually took one tool call.

Paths also solved sharing. We granted individual users or groups read, write, or owner access on a path, and each grant applied to the subtree beneath it. A team could share a whole project with a single grant.

Along with the ltree path, each memory had content, metadata, and an optional event time or validity period. We kept the BM25 keyword search, added embeddings and an HNSW index for semantic search, and combined the two with Reciprocal Rank Fusion for hybrid search.

We also began using the system with coding agents, organizing memories by project, category, and topic. An abbreviated view of a project’s hierarchy looks like this:

share.projects.my_coding_project

├── archived

├── benchmarks

├── docs

│ └── design

├── git_history

└── strategy

├── coding_agent_integration

├── design_principles

└── tool_interface

Agents used the stored context to recover decisions and their rationale across sessions. A project’s codebase could show that service-account creation required an administrator; a stored decision could explain why, which alternatives we had rejected, and what constraints a change needed to preserve. Agents checked that history against the current code and saved new decisions after we settled them.

The tree had real limits. Each memory had exactly one place, and the only structural relationships were ancestors and descendants. Those relationships carried no metadata, so their meaning had to be inferred from the paths. Because grants applied to paths, the hierarchy tended to follow who needed access rather than how the information was related.

Other relationships could live in metadata. For example, our LoCoMo experiments introduced a concrete use for links: reading the conversation around a search hit. We connected conversation turns using prev_id

and next_id

metadata fields and provided a tool to retrieve a message by ID, along with surrounding messages for context. We also used similar pointers for linking commits when importing the Git commit history into a coding project.

IDs in JSONB metadata have no foreign-key constraints, so nothing prevents a pointer to a deleted memory. But memory isn’t a system of record; a dead link is rarely a big deal. A dead link means a missed bit of context, not corrupted data.

A linked list is itself a graph. The tree hadn’t ruled out other relationships; it let us wait until we could name a specific retrieval task they would support.

We had long suspected that a single, sufficiently flexible search backend could serve many of our RAG workloads, and that agent memory was just another one, albeit writable.

Internal teams began using the tree-based system as a RAG backend. Our data team built a semantic catalog of warehouse tables, columns, and views. Marketing organized branding guidance, content, and campaign materials. Engineers stored documentation, Git commits, and coding-agent transcripts. Eon used it to search MCP tool descriptions to find the right tools for a request.

Those applications could reuse our search machinery, but they already had their own users and access policies.

Eon exposed the mismatch. It had access to eon.tools

, but when Susan asked it a question, it needed to know whether Susan could use eon.tools.privileged_tool

. Our grants applied to users of the memory system, but Susan wasn't one. So we added an array of allowed users to each tool’s memory metadata, and Eon filtered its searches to tools the current user could access. Eon owned the policy. The memory system just applied the filter.

Eon’s workaround showed where the boundary belonged. Record operations, hybrid retrieval, and composable filters worked the same regardless of which application called them or who their users were. The embedding machinery’s queueing, retries, and version checks that keep a stale embedding from overwriting newer content were reusable, too. This is the kind of tricky code no one wants to write. Users, permissions, and workflows didn’t carry over; there was too much variation across systems. We extracted the reusable parts into a lightweight library, searchgres.js, and left the rest to applications.

When developing with searchgres.js, an application supplies its content and specifies an embedding model. It can construct a query from user input and known context, or expose tools that let an agent choose search terms and filters. searchgres.js handles record operations and retrieval; the application decides how to organize the information, who can see it, and how to use the results.

Across all three attempts, the useful question wasn’t how expressively a system could represent memories, but which decisions it asked its caller to make. Agents needed to decide what to remember and what to retrieve, but not to invent edge semantics or write SQL. Applications needed to decide who could see what and how retrieval fit their workflows, but not to adopt our model of users and permissions.

searchgres.js is built around that boundary: it handles the search machinery and leaves other decisions to the application.

Read Introducing searchgres.js for the product overview and a working example, or go straight to the getting-started guide. The library is available on npm as searchgres

and on GitHub.

About the author

By John Pruitt

// Related posts

searchgres.js composes BM25, semantic ranking, and filters for hierarchy, metadata, and time in a single Postgres query. Open-source TypeScript, Apache-2.0.

By John Pruitt

October 20th, 2026

Receive the latest technical articles and release notes in your inbox.

Original source: www.tigerdata.com

Technology