-- Ported from the SQLite migration of the same number, and the only one that -- is not a mechanical translation. -- -- SQLite gets full-text search from an FTS5 virtual table with an external -- content table and three triggers keeping it in step. PostgreSQL derives a -- tsvector from the row itself, so there is nothing to keep in step and no -- trigger to forget: the column cannot disagree with the row it is computed -- from. -- -- The search query differs too, and that difference lives in the Go code -- rather than being hidden here — a query that means "match these words" is -- spelled MATCH in one and @@ in the other, and pretending otherwise would -- cost more than admitting it. ALTER TABLE projects ADD COLUMN memory_retention_hours BIGINT; CREATE TABLE memory_notes ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- Scoped to a project and checked in SQL on every read, like threads. project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, -- A namespace inside the project, so two agents sharing a key do not read -- each other's notes. agent_id TEXT NOT NULL, key TEXT NOT NULL, text TEXT NOT NULL, -- Normalised to ",a,b," so a tag matches whole, never as part of another. tags TEXT NOT NULL DEFAULT '', created_at BIGINT NOT NULL, -- Touched on every write, so retention measures how long a note has gone -- unrefreshed rather than how old it is. updated_at BIGINT NOT NULL, -- 'simple' rather than a language configuration: these are an agent's own -- notes, in whatever language it chose, and stemming English rules over -- Turkish text would quietly stop finding things. search tsvector GENERATED ALWAYS AS ( to_tsvector('simple', coalesce(key, '') || ' ' || coalesce(text, '') || ' ' || coalesce(tags, '')) ) STORED, UNIQUE (project_id, agent_id, key) ); CREATE INDEX memory_notes_by_updated ON memory_notes(updated_at); CREATE INDEX memory_notes_search ON memory_notes USING GIN (search);