OpenClaw Agent — Data Model

База: agent_db (PostgreSQL, database-per-service).


ER-диаграмма

erDiagram
    agent_conversations ||--o{ agent_messages : has

    agent_conversations {
        uuid id PK
        uuid user_id "владелец сессии"
        uuid franchise_id "tenant"
        text title "первые 60 символов первого user-message"
        text status "active | closed"
        timestamptz created_at
        timestamptz updated_at
    }

    agent_messages {
        uuid id PK
        uuid conversation_id FK
        text role "user | assistant | tool"
        text content "текст; для tool — JSON-результат"
        jsonb tool_calls "массив, только для role=assistant"
        text tool_call_id "только для role=tool"
        int4 llm_duration_ms "только для role=assistant"
        int4 eval_tokens "только для role=assistant"
        timestamptz created_at
    }

Таблица agent_conversations

ПолеТипConstraintsОписание
idUUIDPK
user_idUUIDNOT NULLВладелец. Источник — JWT
franchise_idUUIDNOT NULLДля мультитенантной изоляции
titleTEXTNOT NULLПервые 60 символов первого user-message, auto
statusTEXTNOT NULL, default active, CHECK in (active, closed)
created_atTIMESTAMPTZNOT NULL, default now()
updated_atTIMESTAMPTZNOT NULL, default now()Обновляется на каждом INSERT в agent_messages через trigger

Индексы:

  • idx_conversations_user_updated ON (user_id, updated_at DESC) — для списка сессий пользователя
  • idx_conversations_franchise ON (franchise_id) — для админ-аудита

Бизнес-правила:

  • Удаление сессии каскадно удаляет все её agent_messages
  • При создании первого user-message сразу же создаётся conversation (один тип запроса в БД — INSERT … ON CONFLICT)
  • closed сессии не принимают новые сообщения (409 CONVERSATION_LIMIT_REACHED)

Таблица agent_messages

ПолеТипConstraintsОписание
idUUIDPK
conversation_idUUIDNOT NULL, FK → agent_conversations(id) ON DELETE CASCADE
roleTEXTNOT NULL, CHECK in (user, assistant, tool, system)
contentTEXTNULLДля assistant с tool_calls — может быть NULL. Для tool — JSON-string результата
tool_callsJSONBNULLМассив [{id, name, arguments_json}], только для role=assistant
tool_call_idTEXTNULLТолько для role=tool — ID вызова, на который отвечаем
llm_duration_msINT4NULLДлительность inference (только assistant)
eval_tokensINT4NULLКол-во сгенерированных токенов (только assistant)
created_atTIMESTAMPTZNOT NULL, default now()

Индексы:

  • idx_messages_conversation_created ON (conversation_id, created_at ASC) — для чтения истории
  • idx_messages_tool_call_id ON (tool_call_id) WHERE role='tool' — для матчинга tool_call → tool_result

Бизнес-правила:

  • role='user' → content NOT NULL, tool_calls NULL, tool_call_id NULL
  • role='assistant' → tool_calls может быть NOT NULL (если модель вызвала tool) ИЛИ content NOT NULL (финальный ответ). Может быть и то, и то.
  • role='tool' → content NOT NULL (JSON-результат), tool_call_id NOT NULL, tool_calls NULL
  • role='system' → внутреннее, не пишется в БД в MVP (системный промпт хардкодом в коде)

Маскирование при чтении:

  • При выдаче через GET /conversations/{id} content для role='tool' отдаётся как есть (это уже маскированный результат — маскирование происходит в OpenClaw Agent перед записью в БД)
  • PII-фильтр в Agent дополнительно маскирует password, pin_hash если приходят в content — двойная защита

Trigger: обновление updated_at в conversations

CREATE OR REPLACE FUNCTION update_conversation_timestamp()
RETURNS TRIGGER AS $$
BEGIN
  UPDATE agent_conversations
  SET updated_at = NEW.created_at
  WHERE id = NEW.conversation_id;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
 
CREATE TRIGGER trg_messages_update_conversation
AFTER INSERT ON agent_messages
FOR EACH ROW EXECUTE FUNCTION update_conversation_timestamp();

Cron-job: auto-cleanup

Раз в сутки (Liquibase + pg_cron / встроенный node-cron):

DELETE FROM agent_conversations
WHERE status = 'closed' AND updated_at < now() - interval '30 days';

Active сессии не трогаем (юзер может вернуться).


Redis-ключи (вспомогательные)

КлючТипTTLНазначение
agent:rate:user:{user_id}:{minute}INT70 секRate-limit per user (30/min)
agent:stream:{conversation_id}STRING (request_id)5 минLock, чтобы юзер не открыл два параллельных стрима на одну сессию

Связь с другими сервисами

СервисЧто хранится у нихЧто у нас
User Serviceusers, permissions_rolesuser_id (denormalized в conversations)
Catalog Serviceproducts, stop_listрезультаты tool в agent_messages.content
Order Serviceordersрезультаты tool, order_id в content (для трассировки)

Ссылки