-- ============================================================================ -- Long-term memory: what an agent may carry from one run into the next. -- -- A run is one turn and agent_runs is an audit record that is never replayed. -- This is the first store whose CONTENTS are deliberately fed back into a -- prompt, which makes it a different kind of table from everything around it -- and is why so much of it is provenance rather than payload. -- -- ORG-SCOPED, per the product decision of 2026-10-07: a memory written while -- one recruiter worked is available to the next, because a workspace's view of -- its own hiring should not reset per seat. I5 still applies — org_id is NOT -- NULL and every read carries the predicate. -- -- WHY `subject_type` AND `subject_id` ARE NOT OPTIONAL. -- Memories are of two kinds and the second one is regulated. A workspace fact -- ("this venue staffs on Thursdays") is operational. An observation about a -- named candidate is personal data that will influence a later hiring answer, -- which under GDPR is profiling and under employment law is an artefact a -- claim can be built on. The distinction has to be queryable, or "show me -- everything held about this person" and "erase it" are not answerable: -- -- SELECT … WHERE subject_type = 'candidate' AND subject_id = $1 -- DELETE … WHERE subject_type = 'candidate' AND subject_id = $1 -- -- so a subject access request and an erasure are each one statement. -- -- WHY `source_run_id` IS NOT OPTIONAL EITHER. A memory that influenced an -- answer must be traceable to the run that wrote it, or "why did it say that" -- stops being answerable the moment memory is involved. ON DELETE SET NULL so -- pruning runs does not destroy the memory, but the column exists so the chain -- is there while the run is. -- -- WHAT THIS TABLE DOES NOT DO. It does not decide. A memory enters a prompt as -- context on the same terms as a retrieved document — fenced, labelled as data -- — and every write still passes the confirmation gate. Nothing here can -- reject a candidate; it can only be read alongside the records. -- ============================================================================ SET search_path = public; CREATE TABLE agent_memories ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- I5. The predicate goes in every read; a memory cannot cross a tenant. org_id uuid NOT NULL REFERENCES organizations (id) ON DELETE CASCADE, -- Who the memory is ABOUT, which is not who wrote it. -- workspace — an operational fact with no personal subject -- candidate — a job_applications or worker_profiles subject -- user — a preference stated by a person about their own working subject_type text NOT NULL, subject_id uuid, -- The memory itself, in the words it will be read back in. text text NOT NULL, -- Provenance. `author` distinguishes a memory a person wrote from one a -- model inferred, because the second needs review and the first does not. author text NOT NULL DEFAULT 'model', source_run_id text REFERENCES agent_runs (run_id) ON DELETE SET NULL, written_by uuid REFERENCES users (id) ON DELETE SET NULL, -- Retrieval, on the same terms as knowledge_chunks so one implementation -- serves both. Vectors from two models are not comparable, hence the model. embedding real[], embedding_model text NOT NULL DEFAULT '', -- Memory decays. A fact with no expiry accumulates forever and is read back -- long after it stopped being true, which is worse than not remembering. created_date timestamptz NOT NULL DEFAULT now(), expires_at timestamptz, -- Soft delete, so an erasure is recorded as having happened rather than -- leaving no trace that anything was there. redacted_at timestamptz, CONSTRAINT agent_memories_subject_check CHECK ( subject_type IN ('workspace', 'candidate', 'user') ), -- A personal memory without a subject cannot be shown to the person it is -- about, which makes it undeletable in practice. Refused at write time. CONSTRAINT agent_memories_subject_id_required CHECK ( subject_type = 'workspace' OR subject_id IS NOT NULL ), CONSTRAINT agent_memories_author_check CHECK (author IN ('model', 'person')), CONSTRAINT agent_memories_text_not_blank CHECK (length(btrim(text)) > 0) ); -- The read path: this tenant's live memories, newest first. CREATE INDEX agent_memories_org_live_idx ON agent_memories (org_id, created_date DESC) WHERE redacted_at IS NULL; -- Subject access and erasure, both of which are by subject. CREATE INDEX agent_memories_subject_idx ON agent_memories (org_id, subject_type, subject_id) WHERE redacted_at IS NULL; -- The sweep that enforces decay. CREATE INDEX agent_memories_expiry_idx ON agent_memories (expires_at) WHERE expires_at IS NOT NULL AND redacted_at IS NULL; COMMENT ON TABLE agent_memories IS 'What an agent may carry between runs. Org-scoped, attributed to a subject so it can be shown and erased, ' 'and traceable to the run that wrote it. Read into prompts as context, never as a decision.';