Dashboard

Component Agents Chatgxy Persistence

ChatGXY persistence model, API flow, and frontend state management for chat conversations

Raw
Revised:
2026-05-21
Revision:
5
Related Notes:
Component - Agents Backend, Component - Agents UX, PR 21434 - AI Agent Framework and ChatGXY, PR 21692 - Standardize Agent API Schemas, PR 21942 - Shared Agent Operations and MCP Server, PR 22070 - Static YAML Agent Backend for Deterministic Testing

ChatGXY Persistence: How Chats Are Stored, Loaded, and Managed

1. Overview / Architecture Summary

Galaxy has two distinct chat UX surfaces that persist conversations:

  1. ChatGXY (/chatgxy) — full-page standalone chat with any agent type. General-purpose Q&A, tool recommendations, error analysis, etc.
  2. PageChatPanel — embedded split-pane chat in the page editor. Scoped to a specific page, talks to the page_assistant agent, and can propose structured edits (diffs).

Both surfaces share the same backend persistence model (ChatExchange + ChatExchangeMessage), the same API endpoint (POST /api/chat), and reuse frontend components (ChatInput, ChatMessageCell, ActionCard).

Data Flow Summary

User types query
    |
    v
Frontend: POST /api/chat  (body: {query, exchange_id?, page_id?})
    |
    v
Backend: ChatAPI.query()
    |--- loads conversation_history from DB if exchange_id present
    |--- calls AgentService.route_and_execute()
    |--- saves exchange: create new or add_message to existing
    |
    v
Response: ChatResponse {response, agent_response, exchange_id}
    |
    v
Frontend: stores exchange_id in ref (+ localStorage for pages)
    |--- pushes ChatMessage into messages array
    |--- renders via ChatMessageCell

Key Architectural Decisions

  • JSON-in-Text storage: Conversation data stored as JSON string in a TEXT column. No separate columns for query/response/agent_type.
  • One exchange = one conversation thread: An exchange has many messages (turns). Each message stores one query+response pair as JSON.
  • No streaming: Request-response only; no WebSocket or SSE for chat (unlike the unmerged PR #21706).
  • No polling: UI does not poll for updates. Messages only appear after explicit user interaction.
  • Soft delete for pages: Page.deleted = True does not cascade to chat exchanges. Frontend cleans up localStorage on page delete; DB rows for the exchange persist (orphaned but harmless).

2. Database Models

Tables

chat_exchange

ColumnTypeConstraintsDescription
idINTEGERPKAuto-incrementing primary key
user_idINTEGERFK -> galaxy_user.id, indexed, NOT NULLOwner of this exchange
job_idINTEGERFK -> job.id, indexed, nullableFor job-based (GalaxyWizard) exchanges
page_idINTEGERFK -> page.id, indexed, nullableFor page-scoped (PageChatPanel) exchanges

Source: lib/galaxy/model/__init__.py lines 3251-3274

Relationships:

  • user -> User (back_populates chat_exchanges)
  • messages -> list[ChatExchangeMessage] (back_populates chat_exchange)
  • page -> Page (no back-population on Page side)

Exchange scoping: Exactly one of job_id, page_id, or neither is set:

  • job_id set: GalaxyWizard error analysis exchange
  • page_id set: PageChatPanel conversation
  • Neither set: General ChatGXY conversation

chat_exchange_message

ColumnTypeConstraintsDescription
idINTEGERPKAuto-incrementing primary key
chat_exchange_idINTEGERFK -> chat_exchange.id, indexedParent exchange
create_timeDATETIMEdefault=nowTimestamp of message creation
messageTEXTJSON-encoded conversation data (see below)
feedbackINTEGERnullable0=negative, 1=positive, NULL=no feedback

Source: lib/galaxy/model/__init__.py lines 3276-3288

Message JSON Structure

Each ChatExchangeMessage.message contains a JSON string with this shape:

{
    "query": "User's question text",
    "response": "Agent's response text",
    "agent_type": "router",
    "agent_response": {
        "content": "...",
        "agent_type": "router",
        "confidence": "high",
        "suggestions": [...],
        "metadata": { "model": "...", "total_tokens": 123, ... },
        "reasoning": "..."
    }
}

The agent_response field is the full serialized AgentResponse Pydantic model (may be null for legacy/error messages). One message = one complete turn (query + response).

Migrations

  1. cbc46035eba0 (2023-06-05): Original migration creating chat_exchange and chat_exchange_message tables. Pre-dates the agent framework.

    • File: lib/galaxy/model/migrations/alembic/versions_gxy/cbc46035eba0_chat_exchange_storage.py
  2. b75f0f4dbcd4 (2025-01-06): Adds page_id column to chat_exchange (plus page model additions: history_id, source_invocation_id, edit_source on page_revision).

    • File: lib/galaxy/model/migrations/alembic/versions_gxy/b75f0f4dbcd4_add_page_history_columns.py
    • Adds FK, index, and constraint for chat_exchange.page_id -> page.id

3. API Layer

All chat endpoints are in lib/galaxy/webapps/galaxy/api/chat.py, class ChatAPI. All marked unstable=True.

Endpoints

MethodPathPurposeUsed By
POST/api/chatMain chat endpoint (send query, get response)ChatGXY, PageChatPanel
GET/api/chat/historyList user’s general (non-job, non-page) chat exchangesChatGXY history sidebar
GET/api/chat/page/{page_id}/historyList page-scoped chat exchangesPageChatPanel on open
DELETE/api/chat/historyClear all non-job chat exchangesChatGXY “Clear History” button
PUT/api/chat/{job_id}/feedbackJob-based feedback (GalaxyWizard)GalaxyWizard
PUT/api/chat/exchange/{exchange_id}/feedbackExchange-based feedbackChatGXY, PageChatPanel
GET/api/chat/exchange/{exchange_id}/messagesGet all messages for an exchangeLoading full conversation on history select

POST /api/chat — Request

Query params: agent_type (default "auto"), optional job_id (legacy)

Body (ChatPayload):

class ChatPayload(Model):
    query: str
    context: Optional[str] = ""
    exchange_id: Optional[int] = None      # Continue existing conversation
    page_id: Optional[DecodedDatabaseIdField] = None  # Scope to page
    regenerate: Optional[bool] = None      # Force fresh (job-based only)

Source: lib/galaxy/schema/schema.py lines 3878-3903

POST /api/chat — Response

class ChatResponse(BaseModel):
    response: str
    error_code: Optional[int]
    error_message: Optional[str]
    agent_response: Optional[AgentResponse] = None
    exchange_id: Optional[int] = None
    processing_time: Optional[float] = None

Source: lib/galaxy/schema/schema.py lines 3906-3935

POST /api/chat — Backend Flow (lines 96-282)

  1. Extract query from body payload or query param
  2. If job_id: check for cached response (skip if regenerate)
  3. Extract exchange_id and page_id from payload
  4. If page_id: load page object, attach history_id and exported page_content to context
  5. If exchange_id: load conversation history from DB as conversation_history in context dict
  6. Call AgentService.route_and_execute() (or legacy fallback)
  7. Save to DB:
    • job_id set -> ChatManager.create(trans, job_id, response)
    • exchange_id set -> ChatManager.add_message(trans, exchange_id, json_data) (append to existing)
    • page_id set (no exchange_id) -> ChatManager.create_page_chat(trans, page_id, ...)
    • Neither -> ChatManager.create_general_chat(trans, query, ...)
  8. Return ChatResponse with exchange_id for conversation continuity

GET /api/chat/history — Response Shape

Returns list[dict] where each dict:

{
    "id": 42,
    "query": "first message query",
    "response": "first message response",
    "agent_type": "router",
    "agent_response": { ... },
    "timestamp": "2026-03-05T10:00:00",
    "feedback": null,
    "message_count": 3
}

Note: only returns data from the first message of each exchange (for history list display). message_count indicates total turns.

GET /api/chat/exchange/{exchange_id}/messages — Response Shape

Returns list[dict] — flattened list of all messages across all turns:

[
    { "role": "user", "content": "...", "timestamp": "..." },
    { "role": "assistant", "content": "...", "agent_type": "router", "agent_response": {...}, "timestamp": "...", "feedback": null },
    { "role": "user", "content": "...", "timestamp": "..." },
    { "role": "assistant", "content": "...", "agent_type": "router", "agent_response": {...}, "timestamp": "...", "feedback": null }
]

Each stored ChatExchangeMessage (one per turn) is split into two entries (user + assistant).


4. Business Logic (ChatManager)

File: lib/galaxy/managers/chat.py

Key Methods

MethodSignatureDescription
create(trans, job_id, message) -> ChatExchangeCreate job-based exchange with raw message string
create_general_chat(trans, query, response_data, agent_type) -> ChatExchangeCreate general ChatGXY exchange (no job, no page)
create_page_chat(trans, page_id, query, response_data, agent_type) -> ChatExchangeCreate page-scoped exchange
add_message(trans, exchange_id, message) -> ChatExchangeMessageAppend a turn to existing exchange (validates ownership)
get(trans, job_id) -> ChatExchange?Lookup by job_id
get_exchange_by_id(trans, exchange_id) -> ChatExchange?Lookup by exchange_id
get_chat_history(trans, exchange_id, format_for_pydantic_ai) -> listGet messages for an exchange. Can return raw dicts or pydantic-ai ModelMessage objects
get_user_chat_history(trans, limit, include_job_chats, include_page_chats) -> list[ChatExchange]List user’s exchanges with filtering
get_page_chat_history(trans, page_id, limit) -> list[ChatExchange]List exchanges for a specific page
set_feedback_for_exchange(trans, exchange_id, feedback) -> ChatExchangeSet feedback (0/1) on first message
set_feedback_for_job(trans, job_id, feedback) -> ChatExchangeSet feedback by job lookup

Important Implementation Details

  • All queries scoped by user_id — users can only see their own exchanges
  • get_user_chat_history defaults to excluding both job chats and page chats (include_job_chats=False, include_page_chats=False)
  • get_chat_history with format_for_pydantic_ai=True converts stored messages into ModelRequest/ModelResponse objects for agent continuation
  • Feedback is stored on ChatExchangeMessage.feedback (the first message’s feedback field). Comment in code: “There is only one message in an exchange currently” — this is outdated since multi-turn was added, but feedback still targets message[0].

5. Frontend State Management

ChatGXY (Standalone Page)

File: client/src/components/ChatGXY.vue

All state is component-local refs — no Pinia store:

RefTypePurpose
messagesref<ChatMessage[]>All displayed messages
currentChatIdref<number | null>Current exchange ID (sent as exchange_id in POST)
chatHistoryref<ChatHistoryItem[]>History sidebar items
busyref<boolean>Loading spinner state
selectedAgentTyperef<string>Dropdown selection (default "auto")
showHistoryref<boolean>History sidebar visibility
loadingHistoryref<boolean>History list loading state
hasLoadedInitialChatref<boolean>Prevents welcome message if latest chat loaded

No localStorage usage in ChatGXY standalone. The currentChatId is ephemeral — lost on page refresh (but the latest chat is reloaded from API on mount).

PageChatPanel (Page Editor)

File: client/src/components/PageEditor/PageChatPanel.vue

Component-local refs plus store-backed persistence:

RefTypePersistencePurpose
messagesref<ChatMessage[]>EphemeralDisplayed messages
currentChatIdref<number | null>Via store -> localStorageCurrent exchange ID
dismissedProposalsref<Set<string>>Via store -> localStorageIDs of proposals user dismissed
busyref<boolean>EphemeralLoading state

pageEditorStore (Pinia)

File: client/src/stores/pageEditorStore.ts

Three useUserLocalStorage entries persist chat state across sessions:

// Per-page chat exchange ID -- survives panel close/reopen
const currentChatExchangeIds = useUserLocalStorage<Record<string, number | null>>(
    "history-page-chat-exchange", {}
);

// Per-page dismissed proposal message IDs
const dismissedChatProposals = useUserLocalStorage<Record<string, string[]>>(
    "history-page-dismissed-proposals", {}
);

// Also: per-history "current page" ID (not chat-specific but relevant)
const currentPageIds = useUserLocalStorage<Record<string, string>>(
    "history-page-current", {}
);

localStorage keys (all user-scoped via hashed user ID prefix):

  • history-page-chat-exchange — Maps pageId -> exchangeId | null
  • history-page-dismissed-proposals — Maps pageId -> messageId[]
  • history-page-current — Maps historyId -> pageId

Store Methods for Chat

MethodSignaturePurpose
getCurrentChatExchangeId(pageId) -> number | nullRead cached exchange ID
setCurrentChatExchangeId(pageId, exchangeId)Write exchange ID to localStorage
clearCurrentChatExchangeId(pageId)Remove exchange ID entry
getDismissedProposals(pageId) -> string[]Read dismissed message IDs
addDismissedProposal(pageId, messageId)Add to dismissed set
clearDismissedProposals(pageId)Remove all dismissed for page
toggleChatPanel()Toggle showChatPanel (mutually exclusive with revisions)

ChatMessage Type

File: client/src/components/ChatGXY/chatTypes.ts

interface ChatMessage {
    id: string;                    // Client-generated (generateId() or hist-* prefix)
    role: "user" | "assistant";
    content: string;
    timestamp: Date;
    agentType?: string;            // e.g. "router", "page_assistant"
    confidence?: string;
    feedback?: "up" | "down" | null;
    agentResponse?: AgentResponse;
    suggestions?: ActionSuggestion[];
    isSystemMessage?: boolean;     // Welcome/system messages don't show feedback
}

useUserLocalStorage

File: client/src/composables/userLocalStorage.ts

Wrapper around browser localStorage that:

  1. Hashes the current user ID
  2. Prefixes all keys with the hash
  3. Returns a reactive Ref<T> that syncs to localStorage

This ensures per-user isolation of stored chat exchange IDs and dismissed proposals.


6. Chat Panel UI Flow

ChatGXY Standalone: Open -> Load -> Display -> Interact

  1. Mount (onMounted):

    • Calls loadLatestChat():
      • GET /api/chat/history?limit=1
      • If data exists, calls loadPreviousChat(latestChat) (which fetches full conversation)
      • Sets hasLoadedInitialChat = true
    • If no chat loaded, pushes a welcome system message
  2. User sends query:

    • Push user ChatMessage to messages array
    • POST /api/chat with {query, exchange_id: currentChatId}
    • On success: store data.exchange_id as currentChatId
    • Push assistant ChatMessage with agentResponse/suggestions
    • Scroll to bottom
  3. Multi-turn: subsequent queries include exchange_id, so backend appends via add_message() and loads conversation history for agent context.

  4. History sidebar:

    • Toggle shows 280px sidebar
    • Lazy-loads via GET /api/chat/history?limit=50
    • Click item -> loadPreviousChat(item):
      • GET /api/chat/exchange/{id}/messages
      • Rebuilds full messages array from all turns
      • Falls back to single-message if full conversation fetch fails
  5. New Chat: Clears messages, resets currentChatId = null, shows new welcome message.

  6. Clear History: DELETE /api/chat/history -> clears sidebar, starts new chat if active.

PageChatPanel: Open -> Load -> Display -> Interact

  1. Mount (onMounted):

    • Calls loadPageChat(): a. Check store.getCurrentChatExchangeId(pageId) (localStorage cache) b. If found, loadConversation(storedExchangeId):
      • GET /api/chat/exchange/{id}/messages
      • Populate messages, set currentChatId, restore dismissed proposals c. If no localStorage hit, fall back to GET /api/chat/page/{page_id}/history?limit=1
      • If data exists, loadConversation(latest.id)
    • If still no messages, show welcome message
  2. User sends query:

    • POST /api/chat with {query, exchange_id: currentChatId, page_id: pageId}
    • On success: store exchange_id in both local ref AND store (setCurrentChatExchangeId)
    • Push assistant message (may contain edit proposals in agentResponse.metadata)
  3. Edit proposals (unique to PageChatPanel):

    • Each assistant message is checked for agentResponse.metadata.edit_mode
    • If full_replacement: shows ProposalDiffView with unified diff
    • If section_patch: shows SectionPatchView with per-section checkboxes
    • Staleness detection: isProposalStale(msg) compares metadata.original_content_hash (DJB2 hash of page content at proposal time) to current content hash
    • Accept: applies content to store (store.updateContent()), saves page with edit_source="agent", adds to dismissed set
    • Reject: adds message ID to dismissed set (localStorage-persisted)
    • Already-applied proposals auto-hide (content equality check)
  4. New Conversation: Clears messages, resets currentChatId, clears dismissed proposals, persists all to localStorage via store.


7. Exchange Lifecycle

Create

First message in a new conversation:

  1. Frontend sends POST /api/chat with exchange_id: null
  2. Backend creates new ChatExchange (via create_general_chat or create_page_chat)
  3. First ChatExchangeMessage attached with JSON payload
  4. exchange.id returned in response
  5. Frontend stores exchange_id in currentChatId ref (+ localStorage for pages)

Continue

Subsequent messages:

  1. Frontend sends POST /api/chat with exchange_id: <existing_id>
  2. Backend loads conversation history from DB for agent context (get_chat_history)
  3. Agent processes query with full conversation history
  4. Backend appends new ChatExchangeMessage via add_message()
  5. Same exchange_id returned

Load (History)

ChatGXY history sidebar:

  1. GET /api/chat/history returns first-message summaries of all general exchanges
  2. User clicks item -> GET /api/chat/exchange/{id}/messages loads all turns
  3. Messages reconstructed into ChatMessage[] with proper roles, types, feedback states

PageChatPanel on open:

  1. Check localStorage for cached exchange_id for this page
  2. If found, load directly via GET /api/chat/exchange/{id}/messages
  3. If not, fall back to GET /api/chat/page/{page_id}/history?limit=1 for most recent

Cleanup

Page deletion (deleteCurrentPage() in pageEditorStore):

  1. Calls DELETE /api/pages/{id} (soft delete: sets page.deleted = True)
  2. Frontend: clearCurrentPageId(historyId), clearCurrentChatExchangeId(pageId), clearDismissedProposals(pageId)
  3. Backend: No cascade to ChatExchange. DB rows persist with page_id pointing to a soft-deleted page.

Chat history clear (DELETE /api/chat/history):

  1. Loads all non-job exchanges for user (up to 1000)
  2. Deletes all ChatExchangeMessage rows, then ChatExchange rows
  3. Hard delete (not soft)
  4. Note: only clears general (non-page, non-job) exchanges

No automatic cleanup for:

  • Orphaned page chat exchanges (page soft-deleted, exchanges remain)
  • Old exchanges with no recent activity
  • There is no TTL, no cron job, no garbage collection

Feedback

Feedback targets the exchange (not individual messages):

  1. Frontend: PUT /api/chat/exchange/{exchange_id}/feedback with body 0 or 1
  2. Backend: sets chat_exchange.messages[0].feedback (first message only)
  3. This is a design limitation — multi-turn exchanges only record feedback on the first message

8. Multi-Exchange Support (History Selection, Switching)

ChatGXY

  • History sidebar (280px, toggled by History button):

    • Lists all general (non-job, non-page) exchanges, most recent first
    • Each item shows: truncated query, agent icon, elapsed time
    • Click loads full conversation and sets currentChatId
    • “Clear History” deletes all general exchanges after confirmation
    • “New” button resets to fresh conversation
  • State on switch:

    • messages array completely replaced
    • currentChatId updated to selected exchange
    • Previous unsent query text lost (no save)
  • No multi-exchange view: Only one conversation visible at a time

PageChatPanel

  • No history sidebar: Only shows the current conversation for the page
  • “New Chat” button: Starts fresh conversation, clears localStorage exchange ID
  • Automatic resume: On panel open, loads the most recent exchange for this page (from localStorage or API)
  • One active exchange per page: The currentChatExchangeIds map stores one exchange ID per page ID

Cross-Page Navigation

When switching between pages in the editor:

  1. Store’s clearCurrentPage() sets showChatPanel = false and clears the exchange ID for that page
  2. Loading a new page does NOT automatically open the chat panel
  3. When user toggles chat for the new page, onMounted in PageChatPanel triggers fresh load
  4. The per-page localStorage mapping ensures each page remembers its own conversation

9. Edit Proposal Persistence

How Proposals Are Created

The page_assistant agent returns structured output via pydantic-ai output types:

# Full replacement
class FullReplacementEdit(BaseModel):
    mode: Literal["full_replacement"]
    reasoning: str
    content: str  # Complete new document

# Section patch
class SectionPatchEdit(BaseModel):
    mode: Literal["section_patch"]
    reasoning: str
    target_section_heading: str
    new_section_content: str

These are serialized into agentResponse.metadata in the DB:

{
    "edit_mode": "full_replacement",
    "content": "...",
    "original_content_hash": "a1b2c3d4"
}

Staleness Detection

Both frontend and backend compute a DJB2 hash of the page content:

# Backend (page_assistant.py)
def _djb2_hash(s: str) -> str:
    h = 5381
    for c in s:
        h = ((h * 33) + ord(c)) & 0xFFFFFFFF
    return format(h, "08x")
// Frontend (PageChatPanel.vue)
function djb2Hash(s: string): string {
    let h = 5381;
    for (let i = 0; i < s.length; i++) {
        h = (h * 33 + s.charCodeAt(i)) >>> 0;
    }
    return h.toString(16).padStart(8, "0");
}

If original_content_hash does not match djb2Hash(currentPageContent), the proposal is marked stale and Accept is disabled.

Dismissed Proposals Persistence

Dismissed proposals are tracked by message ID in localStorage:

  • Key: history-page-dismissed-proposals (user-scoped)
  • Value: { [pageId]: ["msg-123-abc", "hist-assistant-42-1", ...] }
  • Cleared on: page delete, new conversation start
  • Persists across: panel close/reopen, page navigation, browser refresh

10. Key File Paths

Backend

FilePurpose
lib/galaxy/model/__init__.py:3251-3288ChatExchange and ChatExchangeMessage ORM models
lib/galaxy/model/migrations/alembic/versions_gxy/cbc46035eba0_chat_exchange_storage.pyOriginal table migration
lib/galaxy/model/migrations/alembic/versions_gxy/b75f0f4dbcd4_add_page_history_columns.pypage_id column migration
lib/galaxy/managers/chat.pyChatManager — all CRUD operations
lib/galaxy/webapps/galaxy/api/chat.pyChatAPI — all HTTP endpoints
lib/galaxy/schema/schema.py:3878-3935ChatPayload and ChatResponse Pydantic models
lib/galaxy/schema/agents.pyAgentResponse, ActionSuggestion, ActionType
lib/galaxy/agents/page_assistant.pyPageAssistantAgent with structured edit output
lib/galaxy/agents/base.py:186-190AgentType enum including PAGE_ASSISTANT
lib/galaxy/agents/__init__.pyAgent registry with all registered agents
lib/galaxy/webapps/galaxy/services/pages.py:100-109Page soft-delete (no chat cascade)

Frontend

FilePurpose
client/src/components/ChatGXY.vueFull-page chat component (standalone ChatGXY)
client/src/components/ChatGXY/chatTypes.tsChatMessage interface
client/src/components/ChatGXY/chatUtils.tsgenerateId(), scrollToBottom()
client/src/components/ChatGXY/agentTypes.tsAgent type registry, icon/label lookup
client/src/components/ChatGXY/ChatInput.vueShared text input with send button
client/src/components/ChatGXY/ChatMessageCell.vueShared message cell (query/response rendering)
client/src/components/ChatGXY/ActionCard.vueAction suggestion buttons
client/src/components/PageEditor/PageChatPanel.vuePage-scoped chat panel
client/src/components/PageEditor/PageEditorView.vuePage editor (hosts chat panel via EditorSplitView)
client/src/components/PageEditor/EditorSplitView.vueDraggable split pane layout
client/src/components/PageEditor/ProposalDiffView.vueFull-replacement diff UI
client/src/components/PageEditor/SectionPatchView.vueSection-level patch UI
client/src/stores/pageEditorStore.tsPinia store with chat exchange ID and dismissed proposal localStorage
client/src/composables/agentActions.tsAgentResponse type, ActionType enum, action dispatch logic
client/src/composables/userLocalStorage.tsUser-scoped localStorage composable
client/src/components/Page/constants.tsUI strings for chat panel (labels, welcome messages)

11. Gaps and Design Notes

Known Limitations

  1. Feedback granularity: Feedback is per-exchange (stored on message[0]), not per-turn. Multi-turn conversations have one feedback slot for the entire thread.

  2. No cascade on page delete: Page soft-delete does not touch chat_exchange rows. Frontend cleans localStorage but DB rows with page_id pointing to deleted pages persist indefinitely.

  3. History list shows first message only: GET /api/chat/history returns the first query/response pair. The message_count field hints at conversation depth but the sidebar only renders the first query text.

  4. No real-time updates: If the same user opens ChatGXY in two tabs, they see independent local state. No synchronization mechanism exists.

  5. No exchange-level delete: Individual exchanges cannot be deleted through the API. Only “clear all” (DELETE /api/chat/history) is supported, and it only affects general (non-job, non-page) exchanges.

  6. Page chat history not clearable: There is no endpoint to clear page-scoped chat history. The DELETE /api/chat/history endpoint explicitly excludes page chats.

Architecture Observations

  • The JSON-in-Text storage pattern is pragmatic but makes querying individual messages impossible at the SQL level. Searching chat content requires loading and parsing JSON.
  • The ChatManager has three create methods (create, create_general_chat, create_page_chat) that are structurally similar but differ in which FK is set and how the message JSON is constructed. Potential for consolidation.
  • The frontend manages message IDs as client-generated strings (msg-{timestamp}-{random} or hist-{role}-{exchangeId}-{index}). These are purely for Vue’s :key binding and diff rendering — they have no backend counterpart.

Incoming References (6)