Phase 4 — Memory System
Goal
The agent has persistent per-user memory and can read recent chat history without keeping anything in its own context. After this phase the agent can be stateless across sessions — all relevant state lives in this server’s Postgres.
Scope / Deliverables
- Migrations:
memory_kv,chat_messages(full schemas from data-and-contracts.md) - Archive every inbound message into
chat_messages(in the webhook handler, before forwarding to the agent) - Archive every outbound send into
chat_messages(intelegram/outbound.ts) - Tool:
update_memory(single key upsert) - Tool:
store_memory(bulk upsert) - Tool:
get_chat_history(paginated bybeforetimestamp; default limit 50, max 500) - Tool:
get_user_context(user row + full memory map + last 20 messages) - Index check:
chat_messages_user_time_idxkeepsget_chat_historyfast at 100k+ rows
Out of scope for this phase: summarization, vector embeddings, semantic recall. Schema is forward-compatible — chat_summaries and memory_vectors tables can be added later without breaking the KV table.
Automated Unit Testing
-
memory.kv.test.ts—update_memory(userId, key, value)inserts when missing- second
update_memoryto the same(userId, key)overwritesvalueand bumpsupdated_at - reading returns the latest JSON value verbatim (including nested objects and arrays)
- cross-user isolation: same key under two
userIds does not collide
-
store_memory.test.ts—- bulk write of N entries returns
{count: N, updatedAt} - call with empty
entriesarray →INVALID_INPUT(Zod minLength=1) — or returnscount: 0(document which behavior is shipped) - transactional behavior: one malformed entry causes the whole batch to roll back (document and assert the implementation choice)
- bulk write of N entries returns
-
chat_history.test.ts—- default call returns the last 50 messages in chronological order (oldest → newest, or newest → oldest — assert and document)
limit=20returns 20;limit=1000is clamped to 500beforeISO timestamp pages backward correctly; combined withlimitit produces stable pages with no overlap- returns inbound + outbound interleaved with correct
directionfield
-
get_user_context.test.ts—- returns
{user, memory, recentMessages}bundle userblock contains id, username, firstName, status, role, createdAt, approvedAtmemoryis the full KV map for that userrecentMessagesdefaults to the last 20 (chronological); empty user has empty arrays/objects
- returns
-
chat_messages.archive.test.ts—- inbound text message archived with
direction='inbound', kind='text', populatedtextandpayloadcolumns - outbound
send_imagearchived withkind='photo'and the image source inpayload - archive write happens before forwarding to the agent (assert ordering with a probe)
- inbound text message archived with
-
chat_messages.index.test.ts—EXPLAINon theget_chat_historyquery confirms the planner useschat_messages_user_time_idx(sanity test; skip in CI if Postgres is unavailable).
Definition of Done
- Agent writes a memory key, reads it back across separate MCP sessions.
-
get_chat_history(userId, limit=20)returns the last 20 messages in chronological order, both inbound and outbound interleaved. -
get_user_contextreturns coherent bundle (sub-100ms for typical users).