Skip to content

[FEATURE] Add persistent AI conversation data model #329

Description

@MRawhani

Is your feature request related to a problem? Please describe

ai_log is designed for auditing model calls, not retrieving visible chat messages. Building conversation history from it would require repeatedly parsing technical request and response payloads. It also does not reliably represent user messages, message states, or multiple conversations within one study session.

Describe the solution you'd like

Create ai_conversation and ai_message, then extend ai_log.

ai_conversation

Column Sequelize type Nullable
id INTEGER No
userId INTEGER, FK user No
studySessionId INTEGER, FK study_session No
type INTEGER No
deleted BOOLEAN No
deletedAt DATE Yes
createdAt DATE No
updatedAt DATE No

A conversation belongs to one study session but may span multiple study steps.

Conversation types:

const AI_CONVERSATION_TYPES = Object.freeze({
    SIDEBAR: 0,
    ADAPTIVE: 1,
});

Add:

CHECK ("type" IN (0, 1))

ai_message

Column Sequelize type Nullable
id INTEGER No
conversationId INTEGER, FK ai_conversation No
studyStepId INTEGER, FK study_step Yes
documentId INTEGER, FK document Yes
aiModelId INTEGER, FK ai_model Yes
role INTEGER No
content TEXT No
status INTEGER No
metadata JSONB Yes
deleted BOOLEAN No
deletedAt DATE Yes
createdAt DATE No
updatedAt DATE No

Message roles:

const AI_MESSAGE_ROLES = Object.freeze({
    SYSTEM: 0,
    USER: 1,
    ASSISTANT: 2,
});

Message statuses:

const AI_MESSAGE_STATUSES = Object.freeze({
    PENDING: 0,
    COMPLETED: 1,
    FAILED: 2,
    ABORTED: 3,
});

Add:

CHECK ("role" IN (0, 1, 2))
CHECK ("status" IN (0, 1, 2, 3))

User and assistant messages store the model selected for their turn. System context messages use aiModelId = null because the same context may later be used by multiple models.

Messages are ordered by id ASC. Timestamps are used for display and analysis.

Deleting a conversation deletes its messages. Hard-deleting a referenced study step, document, or model sets the corresponding message reference to null so the transcript remains available.

Extend ai_log

Column Sequelize type Nullable

| aiMessageId | INTEGER, FK ai_message | Yes |
| totalLatencyMs | INTEGER | Yes |
| ttftMs | INTEGER | Yes |

ai_log.aiMessageId links a model call to its assistant message.

The new foreign keys use SET NULL: if a conversation or message is hard-deleted, its technical audit logs remain, but the deleted reference becomes null.

ttftMs remains null until streaming is implemented. The existing ai_log.status remains unchanged.

Indexes

Add:

ai_message(conversationId, id)
ai_log(conversationId)
ai_log(requestId)

These support conversation-history loading, request abortion, and the session-wide in-flight check.

Describe alternatives you've considered

Using only ai_log was rejected because it is an audit table containing technical request and response payloads. Retrieving visible conversations from it would require extensive parsing and would still not reliably represent user messages or message states.

Additional context

  • ai_message is the source of truth for visible conversation history and UI states.
  • ai_log is the source of truth for technical request state, auditing, in-flight checks, aborts, tokens, costs, and errors.
  • Follow the existing ai_budget and document constant patterns.

Acceptance criteria

  • Up and down migrations complete successfully.
  • Sequelize associations and constants work.
  • Database checks reject unsupported conversation types, message roles, and message statuses.
  • Messages load deterministically using id ASC.
  • Foreign-key deletion behavior works as described.
  • Existing non-conversation ai_log records remain valid.
  • Relevant model and migration tests pass.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions