Add support for message history pagination, preserve scroll in rendering, and improve draft/message workflows.
CI / check (22) (push) Successful in 9m34s
CI / check (24) (push) Successful in 9m34s

This commit is contained in:
2026-10-06 00:32:18 +02:00
parent 1c0f06716e
commit 387c0ac2e7
40 changed files with 2374 additions and 1314 deletions
+12 -820
View File
@@ -1,3 +1,8 @@
import {buildPersonaSnapshot} from "./personaSnapshot.js";
import {createHistoryRepository} from "./historyRepository.js";
import {createAccountStatements, createAccountRepository} from "./accountRepository.js";
import {createChatStatements, createChatRepository} from "./chatRepository.js";
import {createPersonaStatements, createPersonaRepository} from "./personaRepository.js";
/**
* @file Bündelt Datenbankzugriffe auf Konten, Chats, Nachrichten, Personas und den Marktplatz.
*/
@@ -13,18 +18,6 @@ const parseJsonArray = (value) => {
}
};
const buildPersonaSnapshot = (persona) => JSON.stringify({
name: persona.name,
pronouns: persona.pronouns,
appearance: persona.appearance,
background: persona.background,
details: persona.details,
example_dialogues: persona.example_dialogues,
persona_type: persona.persona_type,
source_market_id: persona.source_market_id ?? null,
source_creator_username: persona.source_creator_username ?? null
});
const MARKET_SORT_ORDERS = {
best: "pm.favorite_count DESC, rating_average DESC, pm.usage_count DESC, datetime(pm.created_at) DESC, pm.id DESC",
newest: "datetime(pm.created_at) DESC, pm.id DESC",
@@ -45,413 +38,10 @@ function normalizeMarketPersonaType(personaType) {
export function createRepositories(db, config) {
const statements = {
insertUser: db.prepare("INSERT INTO users (username, password) VALUES (?, ?)"),
getUser: db.prepare("SELECT username, password, role, session_version FROM users WHERE username = ?"),
updateUsername: db.prepare("UPDATE users SET username = ? WHERE username = ?"),
updatePassword: db.prepare("UPDATE users SET password = ? WHERE username = ?"),
incrementSessionVersion: db.prepare("UPDATE users SET session_version = COALESCE(session_version, 0) + 1 WHERE username = ?"),
insertChatSession: db.prepare(
"INSERT INTO chat_sessions (username, title, assistant_persona_id, user_persona_id, folder_name, is_pinned, archived_at, scenario_prompt, scenario_summary) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
),
listChatSessions: db.prepare(`
SELECT cs.id,
CASE
WHEN cs.assistant_persona_id IS NOT NULL AND cs.source_chat_id IS NULL THEN COALESCE(p.name, cs.title)
ELSE cs.title
END AS title,
cs.title AS stored_title,
cs.assistant_persona_id,
p.name AS assistant_persona_name,
cs.user_persona_id,
up.name AS user_persona_name,
cs.folder_name,
cs.is_pinned,
cs.archived_at,
cs.scenario_prompt,
cs.scenario_summary,
cs.context_summary,
cs.context_summary_message_id,
cs.active_leaf_message_id,
cs.source_chat_id,
cs.branched_from_message_id,
cs.preferred_model,
cs.temperature,
cs.context_length,
cs.response_length,
cs.system_instruction,
cs.created_at,
cs.updated_at
FROM chat_sessions cs
LEFT JOIN personas p ON p.id = cs.assistant_persona_id
LEFT JOIN personas up ON up.id = cs.user_persona_id
WHERE cs.username = ?
ORDER BY cs.is_pinned DESC, datetime(cs.updated_at) DESC
`),
countChatSessions: db.prepare("SELECT COUNT(*) as count FROM chat_sessions WHERE username = ? AND archived_at IS NULL"),
getChatSession: db.prepare(`
SELECT cs.id,
CASE
WHEN cs.assistant_persona_id IS NOT NULL AND cs.source_chat_id IS NULL THEN COALESCE(p.name, cs.title)
ELSE cs.title
END AS title,
cs.title AS stored_title,
cs.assistant_persona_id,
p.name AS assistant_persona_name,
cs.user_persona_id,
up.name AS user_persona_name,
cs.folder_name,
cs.is_pinned,
cs.archived_at,
cs.scenario_prompt,
cs.scenario_summary,
cs.context_summary,
cs.context_summary_message_id,
cs.active_leaf_message_id,
cs.source_chat_id,
cs.branched_from_message_id,
cs.preferred_model,
cs.temperature,
cs.context_length,
cs.response_length,
cs.system_instruction,
cs.created_at,
cs.updated_at
FROM chat_sessions cs
LEFT JOIN personas p ON p.id = cs.assistant_persona_id
LEFT JOIN personas up ON up.id = cs.user_persona_id
WHERE cs.id = ?
AND cs.username = ?
`),
getChatSessionByParticipants: db.prepare(`
SELECT cs.id,
CASE
WHEN cs.assistant_persona_id IS NOT NULL AND cs.source_chat_id IS NULL THEN COALESCE(p.name, cs.title)
ELSE cs.title
END AS title,
cs.title AS stored_title,
cs.assistant_persona_id,
p.name AS assistant_persona_name,
cs.user_persona_id,
up.name AS user_persona_name,
cs.folder_name,
cs.is_pinned,
cs.archived_at,
cs.scenario_prompt,
cs.scenario_summary,
cs.context_summary,
cs.context_summary_message_id,
cs.active_leaf_message_id,
cs.source_chat_id,
cs.branched_from_message_id,
cs.preferred_model,
cs.temperature,
cs.context_length,
cs.response_length,
cs.system_instruction,
cs.created_at,
cs.updated_at
FROM chat_sessions cs
LEFT JOIN personas p ON p.id = cs.assistant_persona_id
LEFT JOIN personas up ON up.id = cs.user_persona_id
WHERE cs.username = ?
AND cs.assistant_persona_id = ?
AND ((cs.user_persona_id IS NULL AND ? IS NULL) OR cs.user_persona_id = ?)
AND cs.source_chat_id IS NULL
ORDER BY cs.is_pinned DESC, datetime(cs.updated_at) DESC
LIMIT 1
`),
updateChatSessionTitle: db.prepare("UPDATE chat_sessions SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"),
updateChatSessionScene: db.prepare("UPDATE chat_sessions SET scenario_prompt = ?, scenario_summary = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"),
updateChatContextSummary: db.prepare("UPDATE chat_sessions SET context_summary = ?, context_summary_message_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"),
updateChatGenerationSettings: db.prepare(`
UPDATE chat_sessions
SET preferred_model = ?, temperature = ?, context_length = ?, response_length = ?, system_instruction = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND username = ?
`),
updateChatPreferredModel: db.prepare("UPDATE chat_sessions SET preferred_model = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"),
updateChatActiveLeaf: db.prepare("UPDATE chat_sessions SET active_leaf_message_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"),
updateChatBranchSource: db.prepare("UPDATE chat_sessions SET source_chat_id = ?, branched_from_message_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"),
setChatActiveLeafById: db.prepare("UPDATE chat_sessions SET active_leaf_message_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"),
updateChatSessionOrganization: db.prepare(`
UPDATE chat_sessions
SET folder_name = ?,
is_pinned = ?,
archived_at = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
AND username = ?
`),
touchChatSession: db.prepare("UPDATE chat_sessions SET updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"),
deleteChatSession: db.prepare("DELETE FROM chat_sessions WHERE id = ? AND username = ?"),
clearAssistantPersonaFromChats: db.prepare(`
UPDATE chat_sessions
SET assistant_persona_id = NULL,
updated_at = CURRENT_TIMESTAMP
WHERE username = ?
AND assistant_persona_id = ?
`),
clearUserPersonaFromChats: db.prepare(`
UPDATE chat_sessions
SET user_persona_id = NULL,
updated_at = CURRENT_TIMESTAMP
WHERE username = ?
AND user_persona_id = ?
`),
insertChatMessage: db.prepare("INSERT INTO chat_messages (chat_id, role, content, model_name, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id, delivery_status, error_message, parent_message_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
updateChatMessageDelivery: db.prepare("UPDATE chat_messages SET delivery_status = ?, error_message = ? WHERE id = ? AND chat_id = ?"),
updateChatMessageSources: db.prepare("UPDATE chat_messages SET sources = ? WHERE id = ? AND chat_id = ?"),
updateChatMessageThinking: db.prepare("UPDATE chat_messages SET thinking = ? WHERE id = ? AND chat_id = ?"),
...createAccountStatements(db),
...createChatStatements(db),
...createPersonaStatements(db),
updateChatMessageLinks: db.prepare("UPDATE chat_messages SET parent_message_id = ?, retry_prompt_message_id = ? WHERE id = ? AND chat_id = ?"),
getChatMessages: db.prepare(`
SELECT thinking, sources, id, role, content, model_name, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id, delivery_status, error_message, parent_message_id, created_at
FROM chat_messages
WHERE chat_id = ?
ORDER BY id
`),
getRecentChatMessages: db.prepare(`
WITH RECURSIVE branch AS (
SELECT cm.* FROM chat_messages cm
JOIN chat_sessions cs ON cs.id = cm.chat_id
WHERE cm.chat_id = ?
AND cm.id = COALESCE(cs.active_leaf_message_id, (SELECT MAX(id) FROM chat_messages WHERE chat_id = cm.chat_id))
UNION ALL
SELECT parent.* FROM chat_messages parent
JOIN branch child ON child.parent_message_id = parent.id
)
SELECT id, role, content FROM branch
WHERE delivery_status != 'failed'
ORDER BY id DESC
LIMIT ?
`),
getRecentChatMessagesUpToId: db.prepare(`
WITH RECURSIVE branch AS (
SELECT * FROM chat_messages WHERE chat_id = ? AND id = ?
UNION ALL
SELECT parent.* FROM chat_messages parent
JOIN branch child ON child.parent_message_id = parent.id
)
SELECT id, role, content FROM branch
WHERE delivery_status != 'failed'
ORDER BY id DESC
LIMIT ?
`),
getChatBranchThroughMessage: db.prepare(`
WITH RECURSIVE branch AS (
SELECT * FROM chat_messages WHERE chat_id = ? AND id = ?
UNION ALL
SELECT parent.* FROM chat_messages parent
JOIN branch child ON child.parent_message_id = parent.id
)
SELECT id, role, content, model_name, retry_variants, retry_active_index, retry_retries_used,
retry_prompt_message_id, delivery_status, error_message, parent_message_id, created_at, sources, thinking
FROM branch
WHERE delivery_status = 'complete'
ORDER BY id
`),
getActiveBranchMessages: db.prepare(`
WITH RECURSIVE branch AS (
SELECT cm.* FROM chat_messages cm
JOIN chat_sessions cs ON cs.id = cm.chat_id
WHERE cm.chat_id = ?
AND cm.id = COALESCE(cs.active_leaf_message_id, (SELECT MAX(id) FROM chat_messages WHERE chat_id = cm.chat_id))
UNION ALL
SELECT parent.* FROM chat_messages parent
JOIN branch child ON child.parent_message_id = parent.id
)
SELECT id, role, content, model_name, retry_variants, retry_active_index, retry_retries_used,
retry_prompt_message_id, delivery_status, error_message, parent_message_id, created_at, sources, thinking
FROM branch ORDER BY id
`),
getSiblingChatMessages: db.prepare(`
SELECT id, role, content, parent_message_id
FROM chat_messages
WHERE chat_id = ? AND role = ? AND parent_message_id IS ?
ORDER BY id
`),
getLatestDescendantMessage: db.prepare(`
WITH RECURSIVE descendants AS (
SELECT id FROM chat_messages WHERE chat_id = ? AND id = ?
UNION ALL
SELECT child.id FROM chat_messages child JOIN descendants parent ON child.parent_message_id = parent.id
WHERE child.chat_id = ? AND child.delivery_status != 'failed'
)
SELECT cm.* FROM chat_messages cm JOIN descendants d ON d.id = cm.id ORDER BY cm.id DESC LIMIT 1
`),
getChatMessageById: db.prepare(`
SELECT thinking, sources, id, role, content, model_name, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id, delivery_status, error_message, parent_message_id, created_at
FROM chat_messages
WHERE id = ?
AND chat_id = ?
`),
getChatMessageByIndex: db.prepare(`
SELECT thinking, sources, id, role, content, model_name, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id, delivery_status, error_message, parent_message_id, created_at
FROM chat_messages
WHERE chat_id = ?
ORDER BY id
LIMIT 1 OFFSET ?
`),
getLatestChatMessage: db.prepare(`
SELECT id, role, content, model_name, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id, delivery_status, error_message, parent_message_id, created_at
FROM chat_messages
WHERE chat_id = ?
ORDER BY id DESC
LIMIT 1
`),
getPreviousUserMessage: db.prepare(`
SELECT id, role, content, model_name, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id, delivery_status, error_message, parent_message_id, created_at
FROM chat_messages
WHERE chat_id = ?
AND role = 'user'
AND id < ?
ORDER BY id DESC
LIMIT 1
`),
listChatMemories: db.prepare(`
SELECT id, username, chat_id, assistant_persona_id, user_persona_id, fact, created_at, updated_at
FROM chat_memories
WHERE chat_id = ?
AND username = ?
ORDER BY datetime(updated_at) DESC, id DESC
`),
getChatMemory: db.prepare(`
SELECT id, username, chat_id, assistant_persona_id, user_persona_id, fact, created_at, updated_at
FROM chat_memories
WHERE id = ?
AND chat_id = ?
AND username = ?
`),
insertChatMemory: db.prepare(`
INSERT INTO chat_memories (username, chat_id, assistant_persona_id, user_persona_id, fact)
VALUES (?, ?, ?, ?, ?)
`),
updateChatMemory: db.prepare(`
UPDATE chat_memories
SET fact = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
AND chat_id = ?
AND username = ?
`),
deleteChatMemory: db.prepare("DELETE FROM chat_memories WHERE id = ? AND chat_id = ? AND username = ?"),
getPersonaChatState: db.prepare(`
SELECT id,
username,
chat_id,
assistant_persona_id,
user_persona_id,
relationship_notes,
tone,
current_location,
goals,
unresolved_threads,
boundaries,
created_at,
updated_at
FROM persona_chat_state
WHERE chat_id = ?
AND username = ?
`),
upsertPersonaChatState: db.prepare(`
INSERT INTO persona_chat_state (
username,
chat_id,
assistant_persona_id,
user_persona_id,
relationship_notes,
tone,
current_location,
goals,
unresolved_threads,
boundaries
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(chat_id) DO UPDATE SET
username = excluded.username,
assistant_persona_id = excluded.assistant_persona_id,
user_persona_id = excluded.user_persona_id,
relationship_notes = excluded.relationship_notes,
tone = excluded.tone,
current_location = excluded.current_location,
goals = excluded.goals,
unresolved_threads = excluded.unresolved_threads,
boundaries = excluded.boundaries,
updated_at = CURRENT_TIMESTAMP
`),
updateChatMessage: db.prepare("UPDATE chat_messages SET content = ? WHERE id = ? AND chat_id = ?"),
updateChatMessageWithRetryState: db.prepare("UPDATE chat_messages SET content = ?, retry_variants = ?, retry_active_index = ?, retry_retries_used = ?, retry_prompt_message_id = ? WHERE id = ? AND chat_id = ?"),
deleteChatMessage: db.prepare("DELETE FROM chat_messages WHERE id = ? AND chat_id = ?"),
deleteChatMessages: db.prepare("DELETE FROM chat_messages WHERE chat_id = ?"),
searchChats: db.prepare(`
SELECT cs.id,
CASE
WHEN cs.assistant_persona_id IS NOT NULL AND cs.source_chat_id IS NULL THEN COALESCE(p.name, cs.title)
ELSE cs.title
END AS title,
cs.title AS stored_title,
cs.assistant_persona_id,
p.name AS assistant_persona_name,
cs.user_persona_id,
up.name AS user_persona_name,
cs.folder_name,
cs.is_pinned,
cs.archived_at,
cs.scenario_prompt,
cs.scenario_summary,
cs.context_summary,
cs.context_summary_message_id,
cs.active_leaf_message_id,
cs.source_chat_id,
cs.branched_from_message_id,
cs.preferred_model,
cs.temperature,
cs.context_length,
cs.response_length,
cs.system_instruction,
cs.created_at,
cs.updated_at,
cm.id AS matched_message_id,
cm.content AS matched_content
FROM chat_sessions cs
LEFT JOIN personas p ON p.id = cs.assistant_persona_id
LEFT JOIN personas up ON up.id = cs.user_persona_id
LEFT JOIN chat_messages cm ON cm.id = (
SELECT m.id FROM chat_messages m
WHERE m.chat_id = cs.id AND LOWER(m.content) LIKE ?
ORDER BY m.id DESC LIMIT 1
)
WHERE cs.username = ?
AND (
LOWER(CASE
WHEN cs.assistant_persona_id IS NOT NULL AND cs.source_chat_id IS NULL THEN COALESCE(p.name, cs.title)
ELSE cs.title
END) LIKE ?
OR cm.id IS NOT NULL
)
ORDER BY cs.is_pinned DESC, datetime(cs.updated_at) DESC
`),
listPersonasByType: db.prepare(`
SELECT id, name, pronouns, appearance, background, details, example_dialogues, persona_type,
source_market_id, source_creator_username, created_at, updated_at
FROM personas
WHERE username = ?
AND persona_type = ?
ORDER BY datetime(updated_at) DESC
`),
listAllPersonas: db.prepare(`
SELECT id, username, name, pronouns, appearance, background, details, example_dialogues, persona_type,
source_market_id, source_creator_username, created_at, updated_at
FROM personas
WHERE username = ?
ORDER BY datetime(updated_at) DESC
`),
getPersona: db.prepare(`
SELECT id, name, pronouns, appearance, background, details, example_dialogues, persona_type,
source_market_id, source_creator_username, created_at, updated_at
FROM personas
WHERE id = ?
AND username = ?
`),
insertPersona: db.prepare("INSERT INTO personas (username, name, pronouns, appearance, background, details, example_dialogues, persona_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"),
insertMarketPersona: db.prepare("INSERT INTO personas (username, name, pronouns, appearance, background, details, example_dialogues, persona_type, source_market_id, source_creator_username) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
getPersonaBySourceMarket: db.prepare(`
SELECT id
@@ -459,61 +49,6 @@ export function createRepositories(db, config) {
WHERE username = ?
AND source_market_id = ?
`),
updatePersona: db.prepare(`
UPDATE personas
SET name = ?,
pronouns = ?,
appearance = ?,
background = ?,
details = ?,
example_dialogues = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
AND username = ?
`),
deletePersona: db.prepare("DELETE FROM personas WHERE id = ? AND username = ?"),
getNextPersonaVersionNumber: db.prepare("SELECT COALESCE(MAX(version_number), 0) + 1 AS nextVersion FROM persona_versions WHERE persona_id = ?"),
insertPersonaVersion: db.prepare("INSERT INTO persona_versions (persona_id, username, version_number, snapshot_json) VALUES (?, ?, ?, ?)"),
listPersonaVersions: db.prepare(`
SELECT id, persona_id, username, version_number, snapshot_json, created_at
FROM persona_versions
WHERE persona_id = ?
AND username = ?
ORDER BY version_number DESC
`),
getPersonaVersion: db.prepare(`
SELECT id, persona_id, username, version_number, snapshot_json, created_at
FROM persona_versions
WHERE id = ?
AND persona_id = ?
AND username = ?
`),
getActivePersonaId: db.prepare("SELECT active_persona_id, active_user_persona_id FROM user_settings WHERE username = ?"),
setActiveUserPersona: db.prepare(`
INSERT INTO user_settings (username, active_user_persona_id)
VALUES (?, ?)
ON CONFLICT(username) DO UPDATE SET active_user_persona_id = excluded.active_user_persona_id
`),
getActiveUserPersona: db.prepare(`
SELECT p.id, p.name, p.pronouns, p.appearance, p.background, p.details, p.example_dialogues
FROM user_settings us
JOIN personas p ON p.id = us.active_user_persona_id
WHERE us.username = ?
`),
getAssistantPersonaForChat: db.prepare(`
SELECT p.id, p.name, p.pronouns, p.appearance, p.background, p.details, p.example_dialogues
FROM chat_sessions cs
JOIN personas p ON p.id = cs.assistant_persona_id
WHERE cs.id = ?
AND cs.username = ?
`),
getUserPersonaForChat: db.prepare(`
SELECT p.id, p.name, p.pronouns, p.appearance, p.background, p.details, p.example_dialogues
FROM chat_sessions cs
JOIN personas p ON p.id = cs.user_persona_id
WHERE cs.id = ?
AND cs.username = ?
`),
listMarketPersonas: db.prepare(`
SELECT pm.id, pm.persona_id, pm.creator_username, pm.name, pm.pronouns, pm.appearance, pm.background, pm.details,
pm.example_dialogues, pm.persona_type, pm.tags, pm.usage_count, pm.favorite_count, pm.rating_total, pm.rating_count,
@@ -691,135 +226,9 @@ export function createRepositories(db, config) {
WHERE creator_username = ?
AND soft_deleted_at IS NULL
`),
updateChatSessionsUsername: db.prepare("UPDATE chat_sessions SET username = ? WHERE username = ?"),
updatePersonasUsername: db.prepare("UPDATE personas SET username = ? WHERE username = ?"),
updatePersonasSourceCreator: db.prepare("UPDATE personas SET source_creator_username = ? WHERE source_creator_username = ?"),
updateUserSettingsUsername: db.prepare("UPDATE user_settings SET username = ? WHERE username = ?"),
updatePersonaMarketCreator: db.prepare("UPDATE persona_market SET creator_username = ? WHERE creator_username = ?"),
updatePersonaVersionsUsername: db.prepare("UPDATE persona_versions SET username = ? WHERE username = ?"),
updatePromptTemplatesUsername: db.prepare("UPDATE prompt_templates SET username = ? WHERE username = ?"),
updateChatMemoriesUsername: db.prepare("UPDATE chat_memories SET username = ? WHERE username = ?"),
updatePersonaChatStateUsername: db.prepare("UPDATE persona_chat_state SET username = ? WHERE username = ?"),
updateMarketFavoritesUsername: db.prepare("UPDATE persona_market_favorites SET username = ? WHERE username = ?"),
updateMarketRatingsUsername: db.prepare("UPDATE persona_market_ratings SET username = ? WHERE username = ?"),
updateMarketReportsUsername: db.prepare("UPDATE persona_market_reports SET reporter_username = ? WHERE reporter_username = ?"),
deleteChatMemoriesByUser: db.prepare("DELETE FROM chat_memories WHERE username = ?"),
deleteChatSessionsByUser: db.prepare("DELETE FROM chat_sessions WHERE username = ?"),
deletePersonasByUser: db.prepare("DELETE FROM personas WHERE username = ?"),
deleteUserSettingsByUser: db.prepare("DELETE FROM user_settings WHERE username = ?"),
deletePersonaVersionsByUser: db.prepare("DELETE FROM persona_versions WHERE username = ?"),
deletePromptTemplatesByUser: db.prepare("DELETE FROM prompt_templates WHERE username = ?"),
deleteMarketPersonasByCreator: db.prepare("DELETE FROM persona_market WHERE creator_username = ?"),
deleteMarketFavoritesByUser: db.prepare("DELETE FROM persona_market_favorites WHERE username = ?"),
deleteMarketRatingsByUser: db.prepare("DELETE FROM persona_market_ratings WHERE username = ?"),
deleteMarketReportsByUser: db.prepare("DELETE FROM persona_market_reports WHERE reporter_username = ?"),
deletePersonaChatStateByUser: db.prepare("DELETE FROM persona_chat_state WHERE username = ?"),
deleteUser: db.prepare("DELETE FROM users WHERE username = ?"),
deleteMarketPersonaByPersonaId: db.prepare("DELETE FROM persona_market WHERE persona_id = ? AND creator_username = ?"),
incrementMarketUsageCount: db.prepare("UPDATE persona_market SET usage_count = usage_count + 1 WHERE id = ?")
};
const renameUserTransaction = db.transaction((currentUsername, nextUsername) => {
// Existing username references do not all cascade on update. Check them at commit,
// after every owned row has been moved to the new username.
db.pragma("defer_foreign_keys = ON");
statements.updateUsername.run(nextUsername, currentUsername);
statements.updateChatSessionsUsername.run(nextUsername, currentUsername);
statements.updatePersonasUsername.run(nextUsername, currentUsername);
statements.updatePersonasSourceCreator.run(nextUsername, currentUsername);
statements.updateUserSettingsUsername.run(nextUsername, currentUsername);
statements.updatePersonaMarketCreator.run(nextUsername, currentUsername);
statements.updatePersonaVersionsUsername.run(nextUsername, currentUsername);
statements.updatePromptTemplatesUsername.run(nextUsername, currentUsername);
statements.updateChatMemoriesUsername.run(nextUsername, currentUsername);
statements.updatePersonaChatStateUsername.run(nextUsername, currentUsername);
statements.updateMarketFavoritesUsername.run(nextUsername, currentUsername);
statements.updateMarketRatingsUsername.run(nextUsername, currentUsername);
statements.updateMarketReportsUsername.run(nextUsername, currentUsername);
});
const deleteUserTransaction = db.transaction((username) => {
statements.deleteMarketFavoritesByUser.run(username);
statements.deleteMarketRatingsByUser.run(username);
statements.deleteMarketReportsByUser.run(username);
statements.deleteMarketPersonasByCreator.run(username);
statements.deletePromptTemplatesByUser.run(username);
statements.deletePersonaVersionsByUser.run(username);
statements.deleteChatMemoriesByUser.run(username);
statements.deletePersonaChatStateByUser.run(username);
statements.deleteChatSessionsByUser.run(username);
statements.deleteUserSettingsByUser.run(username);
statements.deletePersonasByUser.run(username);
return statements.deleteUser.run(username);
});
const completeChatTurnTransaction = db.transaction((chatId, userMessageId, assistantContent, retryState, modelName, sources, thinking) => {
const assistantResult = statements.insertChatMessage.run(
chatId,
"bot",
String(assistantContent || ""),
modelName || null,
JSON.stringify(retryState.retryVariants),
retryState.retryActiveIndex,
retryState.retryRetriesUsed,
retryState.retryPromptMessageId ?? null,
"complete",
null,
userMessageId
);
statements.updateChatMessageDelivery.run("complete", null, userMessageId, chatId);
if (sources) statements.updateChatMessageSources.run(JSON.stringify(sources), assistantResult.lastInsertRowid, chatId);
if (thinking) statements.updateChatMessageThinking.run(thinking, assistantResult.lastInsertRowid, chatId);
statements.setChatActiveLeafById.run(assistantResult.lastInsertRowid, chatId);
return assistantResult;
});
const insertBranchChatMessageTransaction = db.transaction((chatId, role, content, retryState, modelName, deliveryStatus, errorMessage, parentMessageId) => {
const result = statements.insertChatMessage.run(
chatId, role, String(content || ""), modelName || null,
retryState ? JSON.stringify(retryState.retryVariants) : null,
retryState?.retryActiveIndex ?? 0,
retryState?.retryRetriesUsed ?? 0,
retryState?.retryPromptMessageId ?? null,
deliveryStatus,
errorMessage,
parentMessageId ?? null
);
statements.setChatActiveLeafById.run(result.lastInsertRowid, chatId);
return result;
});
const failChatTurnTransaction = db.transaction((chatId, userMessageId, errorMessage, parentMessageId) => {
statements.updateChatMessageDelivery.run("failed", errorMessage, userMessageId, chatId);
statements.setChatActiveLeafById.run(parentMessageId ?? null, chatId);
});
const savePersonaVersion = db.transaction((personaId, username) => {
const persona = statements.getPersona.get(personaId, username);
if (!persona) return null;
const versionNumber = statements.getNextPersonaVersionNumber.get(personaId).nextVersion;
statements.insertPersonaVersion.run(personaId, username, versionNumber, buildPersonaSnapshot(persona));
return versionNumber;
});
const clonePersonaTransaction = db.transaction((personaId, username) => {
const source = statements.getPersona.get(personaId, username);
if (!source) return null;
const cloneId = statements.insertPersona.run(
username,
`${source.name} Copy`,
source.pronouns,
source.appearance,
source.background,
source.details,
source.example_dialogues,
source.persona_type
).lastInsertRowid;
const clone = statements.getPersona.get(cloneId, username);
statements.insertPersonaVersion.run(cloneId, username, 1, buildPersonaSnapshot(clone));
return clone;
});
const importWorkspaceTransaction = db.transaction((username, workspace) => {
const imported = {personas: 0, chats: 0, templates: 0};
const personaIds = new Map();
@@ -965,227 +374,10 @@ export function createRepositories(db, config) {
});
return {
getUser: (username) => statements.getUser.get(username),
insertUser: (username, passwordHash) => statements.insertUser.run(username, passwordHash),
updatePassword: (passwordHash, username) => statements.updatePassword.run(passwordHash, username),
incrementSessionVersion(username) {
statements.incrementSessionVersion.run(username);
return statements.getUser.get(username);
},
renameUser: (currentUsername, nextUsername) => renameUserTransaction(currentUsername, nextUsername),
deleteUser: (username) => deleteUserTransaction(username),
listChats: (username) => statements.listChatSessions.all(username),
searchChats(username, query) {
const like = `%${String(query || "").trim().toLowerCase()}%`;
return statements.searchChats.all(like, username, like);
},
countChats: (username) => statements.countChatSessions.get(username).count,
getChat: (chatId, username) => statements.getChatSession.get(chatId, username),
getChatByParticipants: (username, assistantPersonaId, userPersonaId) => statements.getChatSessionByParticipants.get(username, assistantPersonaId, userPersonaId, userPersonaId),
createChat(username, title, assistantPersonaId = null, userPersonaId = null, scenarioPrompt = null, scenarioSummary = null, organization = {}) {
const chatId = statements.insertChatSession.run(
username,
title,
assistantPersonaId,
userPersonaId,
organization.folderName ?? null,
organization.isPinned ? 1 : 0,
organization.archivedAt ?? null,
scenarioPrompt,
scenarioSummary
).lastInsertRowid;
return statements.getChatSession.get(chatId, username);
},
updateChatTitle: (chatId, username, title) => statements.updateChatSessionTitle.run(title, chatId, username),
updateChatScene: (chatId, username, scenarioPrompt, scenarioSummary) => statements.updateChatSessionScene.run(scenarioPrompt, scenarioSummary, chatId, username),
updateChatContextSummary: (chatId, username, summary, messageId) => statements.updateChatContextSummary.run(summary, Number(messageId || 0), chatId, username),
getPersonaChatState: (chatId, username) => statements.getPersonaChatState.get(chatId, username) || null,
updatePersonaChatState(username, chat, state) {
statements.upsertPersonaChatState.run(
username,
chat.id,
chat.assistant_persona_id ?? null,
chat.user_persona_id ?? null,
state.relationship_notes ?? null,
state.tone ?? null,
state.current_location ?? null,
state.goals ?? null,
state.unresolved_threads ?? null,
state.boundaries ?? null
);
return statements.getPersonaChatState.get(chat.id, username);
},
updateChatOrganization(chatId, username, {folderName = null, isPinned = false, archivedAt = null}) {
return statements.updateChatSessionOrganization.run(folderName, isPinned ? 1 : 0, archivedAt, chatId, username);
},
touchChat: (chatId, username) => statements.touchChatSession.run(chatId, username),
deleteChat: (chatId, username) => statements.deleteChatSession.run(chatId, username),
listChatMessages: (chatId) => statements.getChatMessages.all(chatId),
listActiveBranchMessages: (chatId) => statements.getActiveBranchMessages.all(chatId),
listChatBranchThroughMessage: (chatId, messageId) => statements.getChatBranchThroughMessage.all(chatId, messageId),
listSiblingChatMessages: (chatId, role, parentMessageId) => statements.getSiblingChatMessages.all(chatId, role, parentMessageId ?? null),
getLatestDescendantMessage: (chatId, messageId) => statements.getLatestDescendantMessage.get(chatId, messageId, chatId),
getRecentChatMessages: (chatId) => statements.getRecentChatMessages.all(chatId, config.chatHistoryLimit),
getRecentChatMessagesUpToId: (chatId, messageId) => statements.getRecentChatMessagesUpToId.all(chatId, messageId, config.chatHistoryLimit),
getChatMessage: (chatId, messageId) => statements.getChatMessageById.get(messageId, chatId),
getChatMessageByIndex: (chatId, index) => statements.getChatMessageByIndex.get(chatId, index),
getLatestChatMessage: (chatId) => statements.getLatestChatMessage.get(chatId),
getPreviousUserMessage: (chatId, messageId) => statements.getPreviousUserMessage.get(chatId, messageId),
listChatMemories: (chatId, username) => statements.listChatMemories.all(chatId, username),
getChatMemory: (chatId, memoryId, username) => statements.getChatMemory.get(memoryId, chatId, username),
createChatMemory(username, chat, fact) {
const result = statements.insertChatMemory.run(
username,
chat.id,
chat.assistant_persona_id ?? null,
chat.user_persona_id ?? null,
String(fact || "").trim()
);
return statements.getChatMemory.get(result.lastInsertRowid, chat.id, username);
},
updateChatMemory: (chatId, memoryId, username, fact) => statements.updateChatMemory.run(String(fact || "").trim(), memoryId, chatId, username),
deleteChatMemory: (chatId, memoryId, username) => statements.deleteChatMemory.run(memoryId, chatId, username),
insertChatMessage(chatId, role, content, retryState = null, modelName = null, deliveryStatus = "complete", errorMessage = null, parentMessageId = null) {
return statements.insertChatMessage.run(
chatId,
role,
String(content || ""),
modelName || null,
retryState ? JSON.stringify(retryState.retryVariants) : null,
retryState?.retryActiveIndex ?? 0,
retryState?.retryRetriesUsed ?? 0,
retryState?.retryPromptMessageId ?? null,
deliveryStatus,
errorMessage,
parentMessageId
);
},
insertBranchChatMessage: (chatId, role, content, retryState = null, modelName = null, deliveryStatus = "complete", errorMessage = null, parentMessageId = null) =>
insertBranchChatMessageTransaction(chatId, role, content, retryState, modelName, deliveryStatus, errorMessage, parentMessageId),
updateChatMessageDelivery: (chatId, messageId, deliveryStatus, errorMessage = null) =>
statements.updateChatMessageDelivery.run(deliveryStatus, errorMessage, messageId, chatId),
completeChatTurn: (chatId, userMessageId, assistantContent, retryState, modelName = null, sources = null, thinking = null) =>
completeChatTurnTransaction(chatId, userMessageId, assistantContent, retryState, modelName, sources, thinking),
updateChatMessageThinking: (chatId, messageId, thinking) =>
statements.updateChatMessageThinking.run(thinking, messageId, chatId),
updateChatMessageSources: (chatId, messageId, sources) =>
statements.updateChatMessageSources.run(JSON.stringify(sources), messageId, chatId),
failChatTurn: (chatId, userMessageId, errorMessage, parentMessageId = null) =>
failChatTurnTransaction(chatId, userMessageId, errorMessage, parentMessageId),
updateChatActiveLeaf: (chatId, username, messageId) => statements.updateChatActiveLeaf.run(messageId, chatId, username),
updateChatBranchSource: (chatId, username, sourceChatId, messageId) =>
statements.updateChatBranchSource.run(sourceChatId, messageId ?? null, chatId, username),
updateChatGenerationSettings: (chatId, username, settings) => statements.updateChatGenerationSettings.run(
settings.preferredModel ?? null,
settings.temperature ?? null,
settings.contextLength ?? null,
settings.responseLength ?? null,
settings.systemInstruction ?? null,
chatId,
username
),
updateChatPreferredModel: (chatId, username, model) => statements.updateChatPreferredModel.run(model || null, chatId, username),
listMessageVersions: (messageId) => db.prepare("SELECT * FROM chat_message_versions WHERE message_id = ? ORDER BY id").all(messageId),
saveMessageVersion: (chatId, messageId, version = null, selectedIndex = null) => db.transaction(() => {
const row = statements.getChatMessageById.get(messageId, chatId);
if (!row) throw new Error("Message not found");
let versions = db.prepare("SELECT * FROM chat_message_versions WHERE message_id = ? ORDER BY id").all(messageId);
const insert = db.prepare("INSERT INTO chat_message_versions (message_id, content, thinking, sources, model_name) VALUES (?, ?, ?, ?, ?)");
if (!versions.length) {
db.prepare("INSERT INTO chat_message_versions (message_id, content, thinking, sources, model_name, created_at) VALUES (?, ?, ?, ?, ?, ?)")
.run(messageId, row.content, row.thinking, row.sources, row.model_name, row.created_at);
} else {
// Edits keep their existing semantics: update the selected answer.
db.prepare("UPDATE chat_message_versions SET content = ?, thinking = ?, sources = ?, model_name = ? WHERE id = ?")
.run(row.content, row.thinking, row.sources, row.model_name, versions[row.retry_active_index]?.id ?? versions[0].id);
}
if (version) insert.run(messageId, version.content, version.thinking || null, JSON.stringify(version.sources ?? null), version.modelName);
versions = db.prepare("SELECT * FROM chat_message_versions WHERE message_id = ? ORDER BY id").all(messageId);
const index = version ? versions.length - 1 : selectedIndex;
if (!Number.isInteger(index) || !versions[index]) throw new Error("Version is out of range");
const active = versions[index];
db.prepare(`UPDATE chat_messages SET content = ?, thinking = ?, sources = ?, model_name = ?,
retry_variants = ?, retry_active_index = ?, retry_retries_used = ? WHERE id = ? AND chat_id = ?`)
.run(active.content, active.thinking, active.sources, active.model_name, JSON.stringify(versions.map(item => item.content)), index, versions.length - 1, messageId, chatId);
})(),
updateChatMessage: (chatId, messageId, content) => statements.updateChatMessage.run(content, messageId, chatId),
updateChatMessageRetryState: (chatId, messageId, payload) => statements.updateChatMessageWithRetryState.run(
payload.content,
JSON.stringify(payload.retryVariants),
payload.retryActiveIndex,
payload.retryRetriesUsed,
payload.retryPromptMessageId ?? null,
messageId,
chatId
),
deleteChatMessage: (chatId, messageId) => statements.deleteChatMessage.run(messageId, chatId),
clearChatMessages: (chatId) => statements.deleteChatMessages.run(chatId),
listPersonasByType: (username, personaType) => statements.listPersonasByType.all(username, personaType),
listAllPersonas: (username) => statements.listAllPersonas.all(username),
getPersona: (personaId, username) => statements.getPersona.get(personaId, username),
createPersona(username, payload, personaType) {
const personaId = statements.insertPersona.run(
username,
payload.name,
payload.pronouns,
payload.appearance,
payload.background,
payload.details,
payload.exampleDialogues,
personaType
).lastInsertRowid;
const persona = statements.getPersona.get(personaId, username);
statements.insertPersonaVersion.run(personaId, username, 1, buildPersonaSnapshot(persona));
return persona;
},
updatePersona(personaId, username, payload) {
savePersonaVersion(personaId, username);
return statements.updatePersona.run(
payload.name,
payload.pronouns,
payload.appearance,
payload.background,
payload.details,
payload.exampleDialogues,
personaId,
username
);
},
restorePersonaVersion(personaId, versionId, username) {
const version = statements.getPersonaVersion.get(versionId, personaId, username);
if (!version) return null;
savePersonaVersion(personaId, username);
const snapshot = JSON.parse(version.snapshot_json);
statements.updatePersona.run(
snapshot.name,
snapshot.pronouns ?? null,
snapshot.appearance ?? null,
snapshot.background ?? null,
snapshot.details ?? null,
snapshot.example_dialogues ?? null,
personaId,
username
);
return statements.getPersona.get(personaId, username);
},
listPersonaVersions(personaId, username) {
return statements.listPersonaVersions.all(personaId, username).map((version) => ({
...version,
snapshot: JSON.parse(version.snapshot_json)
}));
},
clonePersona: (personaId, username) => clonePersonaTransaction(personaId, username),
deletePersona: (personaId, username) => statements.deletePersona.run(personaId, username),
clearPersonaReferences(username, personaId) {
statements.clearAssistantPersonaFromChats.run(username, personaId);
statements.clearUserPersonaFromChats.run(username, personaId);
statements.deleteMarketPersonaByPersonaId.run(personaId, username);
},
getActivePersonaIds: (username) => statements.getActivePersonaId.get(username) || {},
setActiveUserPersona: (username, personaId) => statements.setActiveUserPersona.run(username, personaId),
getActiveUserPersona: (username) => statements.getActiveUserPersona.get(username),
getAssistantPersonaForChat: (chatId, username) => statements.getAssistantPersonaForChat.get(chatId, username),
getUserPersonaForChat: (chatId, username) => statements.getUserPersonaForChat.get(chatId, username),
...createHistoryRepository(db),
...createAccountRepository(db, statements),
...createChatRepository(db, config, statements),
...createPersonaRepository(db, statements),
listMarketPersonas({admin = false, username = null, personaType = null, sort = "best"} = {}) {
const normalizedPersonaType = normalizeMarketPersonaType(personaType);
const normalizedSort = normalizeMarketSort(sort);