Multi-conversation AI chat + cross-conversation memory (Bots & Strategies)
Date: 2026-03-10
Project: quantago
Worker version: 86cb0522-04c1-4463-972b-b02dba6bc720
Frontend: https://592e946e.quantago-app.pages.dev
What the user asked for
-
"Ensure the Bot AI Chat has access to the same Tools as well." The bot chat already had a deep toolset (parameters, status, strategy edit, code rewrite). This round added a
remember_facttool so it can record durable facts the same way the strategy AI now can. -
"Save the AI Chat history in a given Strategy as well. Allow opening new conversations for both Strategies and Bots, and switching between conversations." Strategy chats are now persisted, and both editors got a conversation switcher with
+ Newand delete. -
"We should have memory within and cross conversations as well, but focused on the task/strategy/bot at hand." Implemented a 2-tier memory model —
scope='thread'(this conversation) andscope='persistent'(subject-scoped, applies to every future chat about the same bot/strategy).
Schema (migration 0019)
CREATE TABLE ai_conversations (
id UUID PK, user_id, subject_type IN ('bot','strategy'), subject_id,
title, created_at, updated_at
);
CREATE TABLE ai_messages (
id, conversation_id REFERENCES ai_conversations ON DELETE CASCADE,
user_id, role IN ('user','assistant','system'),
content, tool_calls JSONB, model, status, error, created_at
);
CREATE TABLE ai_memories (
id, user_id, subject_type, subject_id,
conversation_id NULL REFERENCES ai_conversations ON DELETE CASCADE,
key, value, created_at
);
subject_id = bots.id (UUID) for bots, strategy_definitions.slug for
strategies. conversation_id IS NULL in ai_memories ⇒ persistent /
cross-conversation; non-null ⇒ thread-scoped.
Backend
-
services/ai-conversations.ts— single service module wrapping all three tables. Functions:ensureDefaultConversation,listConversations,createConversation,deleteConversation,renameConversation,getConversation,listMessages,appendMessage,listMemories,rememberFact,forgetFact,renderMemoryBlock,backfillBotChatIfNeeded. The last one is a one-shot migration that copies legacybot_chat_messagesrows into a fresh "Default" conversation the very first time a user opens the chat after this release — so no history is lost. -
routes/ai/ai-conversations-router.tsmounted at/api/ai:GET /conversations?subjectType=&subjectId=POST /conversationsbody:{ subjectType, subjectId, title? }PATCH /conversations/:idbody:{ title }DELETE /conversations/:idGET /conversations/:id/messagesGET /memories?subjectType=&subjectId=&conversationId=?DELETE /memories/:id
-
Bot chat handler (
routes/bots/bots-chat-handlers.ts):- Accepts
conversationIdin the POST body. If absent, uses the most recently-used conversation (or backfill-creates one). - Streams a new
{ type: 'conversation', id, title }SSE event up front so the client can sync the dropdown when the backend lazily creates a conversation. - All
INSERT INTO bot_chat_messageswrites are replaced withappendMessage. handleListBotChatMessagesaccepts?conversationId=and returns{ conversationId, title, messages: [...] }.handleClearBotChatMessagesaccepts?conversationId=; without it every conversation for this user/bot is deleted (cascade also wipes their messages and thread-scoped memories).- The system prompt has the rendered memory block appended.
- Added
remember_facttoTOOL_DEFINITIONSwithkey,value,scope: 'persistent' | 'thread'. The handler stores it viarememberFact(...)and chooses subject vs thread scope based on the arg.
- Accepts
-
Strategy AI (
routes/strategies/strategies-handlers.ts):- Accepts
conversationIdand persists user + assistant turns throughappendMessage. - The 12-message prior-turn history is pulled from the DB (so reloads keep context) and prepended to the prompt.
STRATEGY_TOOL_SYSTEM_PROMPTgot a "Memory" section instructing the model to emitupdates.remember = { key, value, scope }. This is the Workers AI / llama equivalent of OpenAI's tool-call (since llama doesn't function-call here).- The renderer shows the memory block at the end of the system prompt.
- Accepts
Frontend
-
api-client/index.ts— addedlistAiConversations,createAiConversation,renameAiConversation,deleteAiConversation,listAiConversationMessages,listAiMemories,deleteAiMemory.aiEditStrategy(slug, prompt, conversationId?)and the bot chat helpers (fetchBotChatMessages,clearBotChatMessages,streamBotChatMessage) now take an optionalconversationId. -
StrategyEditorPage.tsx— chat panel header now has a<select>of conversations,+ NewandDeletebuttons, and a "📌 Memory (n)" surface showing the top 5 persistent facts. Switching the dropdown re-seedsturnsfrom the server. After every send, if the model emittedupdates.remember, memories are refreshed. -
BotDetailPage.tsx— same conversation switcher above theStreamingChatPanel, with a small📌 N memorieschip that surfaces a tooltip listing every fact. The streamedconversationSSE event is consumed to syncactiveConversationIdwhen the backend lazily creates a conversation. EverystreamBotChatMessagecall now passes the activeconversationId. The handler is properly typed viaChatStreamHandlerexported from@quantago/chat-ui.
Notes / decisions
- The ID model uses slug for strategies and UUID for bots — kept
the existing
subject_id TEXTcolumn to stay polymorphic without two separate FK columns. backfillBotChatIfNeededruns at most once per (user, bot) and is a no-op after the first call. We accept its silent failure (warn + carry on) since the worst case is an empty initial conversation.- The bot toolset already had everything the strategy AI gained earlier
(update_strategy_metadata, update_strategy_parameter_schema,
update_strategy_code), so the only real "tool parity" addition this
round is
remember_fact. The user's first ask is now satisfied. - The strategy AI emits
updates.rememberinstead of an OpenAI-style tool call because Workers AI llama doesn't expose a function-calling API — the JSON-tool-protocol parser already in place was a natural fit.