Back to blog
EngineeringJan 20, 2025· min read

Memory System Maturation: From Prototype to Production

Three memory system fixes in one week. Not a brag—a symptom. This is what maturation looks like for an agentic system built in public.

When the Seams Start Showing

I shipped three memory system fixes in the span of a week. That's not a brag—it's a symptom.

The soft delete bug where deleted entities leaked back into queries (PR #279). The UI consolidation work to eliminate dual query paths that showed different data on different screens (PR #277). And before that, the memory subtask linkage fix that prevented context loss during mission decomposition (PR #273).

Three separate PRs. Three different failure modes. But really, one underlying story: Sabine's memory system grew from prototype to production faster than I realized, and I'm playing catch-up on the architectural debt.

This is what maturation looks like for an agentic system built in public. Not a clean migration plan. Not a roadmap item. Just the honest work of noticing where the seams are showing and stitching them back together.

The Ghost in the Machine

The soft delete bug was the one that got my attention.

We use a standard soft-delete pattern: set is_active = false instead of actually removing rows. It's table stakes for any system that needs audit trails or undo capability. I'd implemented it across the memory tables months ago.

But I'd missed something subtle: the is_active filter wasn't consistently applied across all query paths. It was in some places—the main memory router, the graph builder queries. But not everywhere.

So deleted entities would disappear from one part of the UI, then reappear in another. Users would see ghosts—entities they'd explicitly removed, still showing up in the entity grid or surfacing in agent context windows.

The fix was straightforward: restore the is_active = true filter in five places across push_back.py, query.py, graph_builder.py, the memory router, and the EntityGrid component. Pure SQL hygiene.

But the fact that it needed fixing? That's the interesting part.

The Divergence Problem

The UI consolidation fix was architecturally more significant.

We had two different paths for querying memory data. The main dashboard view went through the backend memory API—proper authentication, consistent filtering, logged queries. But the dedicated /dashboard/memory page made direct Supabase queries from the frontend.

It seemed fine when I built it. Less latency. One fewer hop. Simpler data flow for that specific page.

Except the two paths diverged. Different filters. Different sort orders. Different visibility rules. You'd look at the entity count on the dashboard and see one number. Navigate to the memory page and see a different number. Same data source. Different queries. Different results.

That's not a bug in the traditional sense—both queries worked correctly according to their own logic. But it violated the principle of a single source of truth. And it made debugging impossible because I couldn't trust what I was seeing.

The fix was to rip out the direct Supabase path and route everything through the backend memory API. One query path. One set of filters. One version of the truth.

It cost some latency. I'll optimize that later. Right now, I needed consistency more than I needed speed.

Context Loss During Decomposition

The mission subtask linkage bug was different—more about system boundaries than data integrity.

When Strug Works decomposes a mission into subtasks, each subtask becomes its own agent execution. That's the whole point—parallel execution, isolated failure domains, clear ownership.

But the original implementation lost the parent mission context during that handoff. The subtask would execute successfully, but we had no way to roll up its completion status to the parent mission. From the mission dashboard perspective, subtasks would just vanish into the ether.

The fix required touching four places: task_queue.py to preserve the mission_id during enqueuing, orchestrator.py to thread it through decomposition, the missions router to validate lineage, and a database migration to make mission_id actually work as a foreign key.

It's working now. But it exposed something I hadn't fully designed: what does mission progress tracking actually mean in a system where tasks decompose recursively? Do subtasks of subtasks still link back to the original mission? How many levels deep?

I don't have those answers yet. But at least now I'm asking the right questions.

What Maturation Actually Looks Like

None of these bugs were catastrophic. The system kept running. Agents kept executing tasks. Sabine kept managing my schedule and email.

But they were friction. The kind of friction that compounds. The ghost entities made users question what was real. The UI divergence made me second-guess my own debugging. The context loss made mission tracking feel like a half-implemented feature.

This is the phase where a system transitions from "it works on my machine" to "it works reliably for everyone." You're not adding features. You're not optimizing performance. You're just making the system do what it already should be doing, but consistently.

It's unglamorous work. No one writes blog posts titled "We Fixed Three Medium-Sized Bugs." But it's the work that turns a prototype into a product.

The Luxury of Eating Your Own Dog Food

Here's the advantage of running an agentic organization: every bug I fix in Sabine's memory system, I discover by using Sabine.

I'm not relying on user reports or monitoring dashboards. I'm the user. When entities leak into my context window, I see them immediately. When the dashboard shows inconsistent counts, I notice because I'm looking at it daily.

This is the "proof of production" part of our model. We don't ship tools we haven't already used ourselves. Sabine is both the product and the test bed.

It means I find bugs faster. It also means I can't ignore them.

What's Next

The architectural debt is clearing, but it's revealing the next layer of work.

I need unified caching across memory queries so we're not hitting the database on every page load. I need real-time updates so the UI reflects memory changes without manual refresh. I need audit logging for all memory mutations so I can debug state changes retroactively.

And underneath all of that, I need to formalize the memory schema itself. Right now it's a loose collection of tables with implicit relationships. That worked for the prototype. It won't work for the scale I'm building toward.

But that's the next post.

For now, three bugs fixed. Three seams stitched. One system closer to production-ready.

That's the work.

---

This is part of an ongoing series documenting the operational evolution of Strug City's agentic infrastructure. For the technical implementation details, see PR #279 (soft delete), PR #277 (UI consolidation), and PR #273 (mission linkage) in the Sabine repository.