Skip to main content

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​

  1. "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_fact tool so it can record durable facts the same way the strategy AI now can.

  2. "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 + New and delete.

  3. "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) and scope='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 legacy bot_chat_messages rows 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.ts mounted at /api/ai:

    • GET /conversations?subjectType=&subjectId=
    • POST /conversations body: { subjectType, subjectId, title? }
    • PATCH /conversations/:id body: { title }
    • DELETE /conversations/:id
    • GET /conversations/:id/messages
    • GET /memories?subjectType=&subjectId=&conversationId=?
    • DELETE /memories/:id
  • Bot chat handler (routes/bots/bots-chat-handlers.ts):

    • Accepts conversationId in 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_messages writes are replaced with appendMessage.
    • handleListBotChatMessages accepts ?conversationId= and returns { conversationId, title, messages: [...] }.
    • handleClearBotChatMessages accepts ?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_fact to TOOL_DEFINITIONS with key, value, scope: 'persistent' | 'thread'. The handler stores it via rememberFact(...) and chooses subject vs thread scope based on the arg.
  • Strategy AI (routes/strategies/strategies-handlers.ts):

    • Accepts conversationId and persists user + assistant turns through appendMessage.
    • The 12-message prior-turn history is pulled from the DB (so reloads keep context) and prepended to the prompt.
    • STRATEGY_TOOL_SYSTEM_PROMPT got a "Memory" section instructing the model to emit updates.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.

Frontend​

  • api-client/index.ts — added listAiConversations, createAiConversation, renameAiConversation, deleteAiConversation, listAiConversationMessages, listAiMemories, deleteAiMemory. aiEditStrategy(slug, prompt, conversationId?) and the bot chat helpers (fetchBotChatMessages, clearBotChatMessages, streamBotChatMessage) now take an optional conversationId.

  • StrategyEditorPage.tsx — chat panel header now has a <select> of conversations, + New and Delete buttons, and a "📌 Memory (n)" surface showing the top 5 persistent facts. Switching the dropdown re-seeds turns from the server. After every send, if the model emitted updates.remember, memories are refreshed.

  • BotDetailPage.tsx — same conversation switcher above the StreamingChatPanel, with a small 📌 N memories chip that surfaces a tooltip listing every fact. The streamed conversation SSE event is consumed to sync activeConversationId when the backend lazily creates a conversation. Every streamBotChatMessage call now passes the active conversationId. The handler is properly typed via ChatStreamHandler exported from @quantago/chat-ui.

Notes / decisions​

  • The ID model uses slug for strategies and UUID for bots — kept the existing subject_id TEXT column to stay polymorphic without two separate FK columns.
  • backfillBotChatIfNeeded runs 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.remember instead 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.