DEV Community

Discussion on: Amnesia and déjà vu: how our AI agents failed after every restart (and how we fixed it)

Collapse
 
cekuu35 profile image
Cenk KURTOĞLU •

Moving the memory from RAM to Postgres is the right call, but it quietly reclassifies the agent’s memory as user data — and that changes who has to protect it. Once runs, tool-call records and conversation state live in Postgres, they need the same isolation you’d demand of any tenant table: if another run (or another tenant’s agent) can read a row it shouldn’t, that’s the identical failure class as a missing RLS policy — just on the tables nobody thinks to put RLS on. Two things I’d look at given this design: (1) the “every tool it calls is recorded” table is now the thing you trust to decide whether a replay is safe, so it has to be append-only and not writable on the agent/client path — otherwise a compromised run edits its own history and the safety check waves through a replay that already sent the email twice; (2) if that memory is read back through the Data API, make sure it isn’t caught by a permissive policy “because it’s internal.” The amnesia/déjà vu framing is the clearest way I’ve seen the replay problem stated — it’s the same shape as webhook idempotency, you’re answering is it safe to apply this again, not where do we resume.

Collapse
 
anis_meziani_52aab42304a8 profile image
Anis Meziani •

Thanks, this is a sharp read. You’re right that persisting memory turns it into user data, with the same isolation bar as any tenant table.

We actually hit that exact failure class. Our ADK-native session routes originally checked that a token was valid, not which user it acted for, so the user_id in the path or body was trusted. We now enforce that the token’s subject matches the user_id on every route that reads sessions or memory, including the live WebSocket, and run records are filtered by owner on every read and replay. There’s no auto-generated Data API in front of Postgres either: tables are only reachable through the backend, so there’s no permissive policy to forget. Isolation lives in the application layer today, and RLS as defense in depth is a fair next step.

On (1): the tool record is never accepted from a client or written by the agent. It’s derived server-side from the run’s events, and if that history can’t be read, or has been deleted, the result is “unknown”, which refuses the replay. What we don’t have is a database-level guarantee, like making that column write-once for the app role. Agreed it would be stronger.

If you’d like to dig into either of those, an issue or a PR would be very welcome: github.com/apowerb/apowerb

Collapse
 
cekuu35 profile image
Cenk KURTOĞLU •

Thanks for taking it seriously enough to check, and for the detail — enforcing that the token subject matches the user_id on every session and memory route, including the live WebSocket, is the right fix, and having no auto-generated Data API in front of Postgres removes the whole permissive-policy class. The gap your own note names is the real one: "derived from the run's events, else unknown" is a solid app-level guarantee but not a database one, and a write-once column (an INSERT-only grant for the app role, or a trigger that forbids updates) is what makes it checkable by the database instead of by convention. Happy to open an issue on apowerb with the shape if that's useful to you.

Thread Thread
 
anis_meziani_52aab42304a8 profile image
Anis Meziani •

Yes please, open it, and thanks. Two details from the code so it starts on the right shape:

  1. An INSERT-only grant won’t fit as is. The row is inserted when the run starts, with tools_executed NULL, and the column is written once at the end (settle_run). So the rule is closer to a trigger that allows NULL to a value exactly once and rejects any later change. NULL already means unknown and blocks the replay, so “set once” is the only legal transition.

  2. The bigger part: today the app role creates agent_runs itself at startup (create_all plus ensure_columns), so it owns the table and could drop that trigger. For it to be checkable by the database and not by convention, ownership has to move to a migration role, with the app role keeping only what it needs.

If you shape the issue around those two, it’s a clean one to pick up, and it’s yours to take if you want to go as far as the PR. Credited either way.

it was a great discussion ;)

Thread Thread
 
anis_meziani_52aab42304a8 profile image
Anis Meziani •

Cenk, your idea is now two issues on the repo, split the way we discussed:

Both credit you. The first one is small, self-contained and fully testable, and it’s the guarantee that decides whether an agent can safely replay a failed run in production. Few first PRs touch something that central. If you want it, it’s yours: the tech team will review it and help you along the way.

Thanks again for pushing on this, your comment made the design better.