Changeset 0.8.0 (#39)

This commit is contained in:
2026-02-21 16:53:07 +00:00
parent b72bfbb5b4
commit 494b1aa981
14 changed files with 1510 additions and 150 deletions

View File

@@ -113,7 +113,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
}
// Load conversation history
messages, err := h.loadConversation(channelID, presetSystemPrompt)
messages, err := h.loadConversation(channelID, userID, presetSystemPrompt)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load conversation"})
return
@@ -126,7 +126,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
})
// Persist user message
if err := h.persistMessage(channelID, "user", req.Content, "", 0, 0); err != nil {
if _, err := h.persistMessage(channelID, userID, "user", req.Content, "", 0, 0, nil); err != nil {
log.Printf("Failed to persist user message: %v", err)
}
@@ -160,9 +160,9 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
}
if stream {
h.streamCompletion(c, provider, providerCfg, provReq, channelID, model)
h.streamCompletion(c, provider, providerCfg, provReq, channelID, userID, model)
} else {
h.syncCompletion(c, provider, providerCfg, provReq, channelID, model)
h.syncCompletion(c, provider, providerCfg, provReq, channelID, userID, model)
}
}
@@ -173,7 +173,7 @@ func (h *CompletionHandler) streamCompletion(
provider providers.Provider,
cfg providers.ProviderConfig,
req providers.CompletionRequest,
channelID, model string,
channelID, userID, model string,
) {
ch, err := provider.StreamCompletion(c.Request.Context(), cfg, req)
if err != nil {
@@ -228,7 +228,7 @@ func (h *CompletionHandler) streamCompletion(
// Persist assistant response
if fullContent != "" {
if err := h.persistMessage(channelID, "assistant", fullContent, model, 0, 0); err != nil {
if _, err := h.persistMessage(channelID, userID, "assistant", fullContent, model, 0, 0, nil); err != nil {
log.Printf("Failed to persist assistant message: %v", err)
}
}
@@ -241,7 +241,7 @@ func (h *CompletionHandler) syncCompletion(
provider providers.Provider,
cfg providers.ProviderConfig,
req providers.CompletionRequest,
channelID, model string,
channelID, userID, model string,
) {
resp, err := provider.ChatCompletion(c.Request.Context(), cfg, req)
if err != nil {
@@ -250,7 +250,7 @@ func (h *CompletionHandler) syncCompletion(
}
// Persist assistant response
if err := h.persistMessage(channelID, "assistant", resp.Content, model, resp.InputTokens, resp.OutputTokens); err != nil {
if _, err := h.persistMessage(channelID, userID, "assistant", resp.Content, model, resp.InputTokens, resp.OutputTokens, nil); err != nil {
log.Printf("Failed to persist assistant message: %v", err)
}
@@ -394,7 +394,10 @@ func (h *CompletionHandler) resolveConfig(userID string, channelID string, req c
// ── Conversation Loader ─────────────────────
func (h *CompletionHandler) loadConversation(channelID string, presetSystemPrompt string) ([]providers.Message, error) {
// loadConversation builds the message history for the LLM by walking the
// active path through the message tree. Only the current branch is sent —
// sibling branches are never included in context.
func (h *CompletionHandler) loadConversation(channelID, userID, presetSystemPrompt string) ([]providers.Message, error) {
// Load system prompt from channel
var systemPrompt *string
_ = database.DB.QueryRow(
@@ -416,23 +419,20 @@ func (h *CompletionHandler) loadConversation(channelID string, presetSystemPromp
})
}
// Load message history (oldest first)
rows, err := database.DB.Query(`
SELECT role, content FROM messages
WHERE channel_id = $1
ORDER BY created_at ASC
`, channelID)
// Walk the active path (root → leaf) instead of loading all messages
path, err := getActivePath(channelID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var msg providers.Message
if err := rows.Scan(&msg.Role, &msg.Content); err != nil {
return nil, err
for _, m := range path {
if m.Role == "system" {
continue // system prompts handled above
}
messages = append(messages, msg)
messages = append(messages, providers.Message{
Role: m.Role,
Content: m.Content,
})
}
return messages, nil
@@ -440,42 +440,59 @@ func (h *CompletionHandler) loadConversation(channelID string, presetSystemPromp
// ── Message Persistence ─────────────────────
func (h *CompletionHandler) persistMessage(channelID, role, content, model string, inputTokens, outputTokens int) error {
// persistMessage inserts a message into the tree, updates the cursor, and
// returns the new message's ID.
//
// Parent resolution:
// - parentOverride (explicit parent for edit/regen operations)
// - cursor's active_leaf_id (normal conversation flow)
// - latest message by created_at (fallback for legacy/missing cursor)
func (h *CompletionHandler) persistMessage(channelID, userID, role, content, model string, inputTokens, outputTokens int, parentOverride *string) (string, error) {
var tokensUsed *int
if inputTokens > 0 || outputTokens > 0 {
total := inputTokens + outputTokens
tokensUsed = &total
}
// Find current leaf for parent_id
// Determine parent
var parentID *string
_ = database.DB.QueryRow(
`SELECT id FROM messages WHERE channel_id = $1 ORDER BY created_at DESC LIMIT 1`,
channelID,
).Scan(&parentID)
if parentOverride != nil {
parentID = parentOverride
} else {
parentID, _ = getActiveLeaf(channelID, userID)
}
// Compute sibling_index
siblingIdx := nextSiblingIndex(channelID, parentID)
// Determine participant
participantType := "user"
participantID := channelID // will be overridden below
participantID := userID
if role == "assistant" {
participantType = "model"
participantID = model
} else {
// Get channel owner as participant_id for user messages
var ownerID string
if err := database.DB.QueryRow(`SELECT user_id FROM channels WHERE id = $1`, channelID).Scan(&ownerID); err == nil {
participantID = ownerID
}
}
_, err := database.DB.Exec(`
INSERT INTO messages (channel_id, role, content, model, tokens_used, parent_id, participant_type, participant_id)
VALUES ($1, $2, $3, NULLIF($4, ''), $5, $6, $7, $8)
`, channelID, role, content, model, tokensUsed, parentID, participantType, participantID)
// Insert with RETURNING id
var newID string
err := database.DB.QueryRow(`
INSERT INTO messages (channel_id, role, content, model, tokens_used,
parent_id, participant_type, participant_id, sibling_index)
VALUES ($1, $2, $3, NULLIF($4, ''), $5, $6, $7, $8, $9)
RETURNING id
`, channelID, role, content, model, tokensUsed,
parentID, participantType, participantID, siblingIdx,
).Scan(&newID)
if err != nil {
return err
return "", err
}
// Update cursor to point at new message
if userID != "" {
_ = updateCursor(channelID, userID, newID)
}
// Touch channel updated_at
_, _ = database.DB.Exec(`UPDATE channels SET updated_at = NOW() WHERE id = $1`, channelID)
return nil
return newID, nil
}