July 8, 2026

Every table finally has one owner

The raw-SQL sweep that started with memory reached the rest of the app — scheduler, settings, policy, MCP, skills, documents — plus a couple of bugs that had been quietly breaking things for a while.

28 commits. One theme: nothing hand-rolls its own SQL anymore.

The memory rework gave facts, places, contacts, and documents their own homes. Today that same idea went everywhere else. Scheduler, settings, tool configs, telemetry, policy, watched folders, MCP connections, skills — each was a service reaching into the database directly, writing its own SELECTs and UPSERTs by hand, in whatever style that file’s author was in that day. Now each table has exactly one model that owns its SQL, and the service that used to write queries just calls methods on it. Boring to describe. Huge to trust.

What matters more is the query builder underneath getting real teeth. Updates and deletes with no predicate now throw instead of quietly running against every row in the table — a mistake that used to be possible is now a crash at the call site, which is where you want to find it. Column names and sort directions get checked against a whitelist instead of trusted.

Which is how the day’s ugliest bug turned up. order_by, one path in that same builder, was still taking whatever string it was handed and gluing it straight into the SQL, right next to sibling methods already locked down. Nobody was exploiting it. It was just sitting there, unguarded, in code that’s supposed to be the safe way to talk to the database. Fixed the same way as everything around it: parse it, whitelist the column and direction, reject anything else. That’s the kind of hole that turns into a real incident the day someone finds it before you do.

Two more bugs came out of just moving code and watching it break. MCP tool syncing had never been pointed at the right database file — silently writing to the main one instead of the file that actually holds tool data, so every sync or read was quietly wrong from day one. A document endpoint was returning tags and metadata as raw JSON strings instead of parsed objects, crashing the moment anything tried to use them. Both had been broken a while. Both only surfaced because moving the SQL onto a model forced someone to actually look at what it was doing.

One smaller fix in the memory graph: the generic “system” row kind was doing two unrelated jobs — real searchable memories and internal bookkeeping like cursors and timers — told apart only by a string prefix sniffed at query time. Split into two actual kinds now. One gets indexed and recalled, one doesn’t, and the difference is structural instead of a naming convention.

One real feature rode along: scheduled tasks now get a name the moment you create them, generated straight from what you asked for — not a placeholder that fills in later once a background job gets around to it. Small thing, but it’s the difference between a task list that looks finished immediately and one that looks half-built for the first few minutes.

What’s left is deliberately raw — joins, aggregates, the vector-search shadow tables — because the builder genuinely can’t express those cleanly yet. That’s an honest boundary, not an unfinished one. With the persistence layer this uniform, the next pass is about behavior, not archaeology.

  • Moved scheduler, settings, tool configs, telemetry, policy, watched folders, MCP connections, and skills off hand-rolled SQL onto owning models — the last major services still writing raw queries directly

  • Locked down the query builder itself: predicate-less updates/deletes now throw instead of silently hitting every row, and closed an unguarded raw-SQL injection point in the sort-order path

  • Fixed MCP tool sync silently writing to the wrong database file, and a document endpoint crashing on unparsed JSON fields — both invisible until the surrounding code got moved and re-read

  • Split a conflated internal memory-graph kind into two real ones, so searchable memory and internal bookkeeping stop being told apart by a string prefix

  • Scheduled tasks now get a real name at creation time, generated from the prompt, instead of waiting on a background pass to label them