Mercurial
diff mrjunejune/conversation_store.c @ 260:1f9877b637e9
Add Copilot-powered cyberpunk JRPG chat
Integrate the production JRPG chat with Seobeo streaming, Deita persistence, and a Bazel-managed Copilot SDK and LiteLLM inference stack.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <mrjunejune@users.noreply.github.com> |
|---|---|
| date | Wed, 05 Aug 2026 09:19:41 -0700 |
| parents | |
| children | 04fee26ecce0 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/conversation_store.c Wed Aug 05 09:19:41 2026 -0700 @@ -0,0 +1,496 @@ +#include "mrjunejune/conversation_store.h" + +#include "deita/deita.h" + +#include <fcntl.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +struct Conversation_Store { + Deita_Connection *p_connection; + pthread_mutex_t mutex; +}; + +boolean Conversation_Store_Generate_UUID(char output[37]) +{ + uint8 bytes[16]; + int fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) + return FALSE; + size_t offset = 0; + while (offset < sizeof(bytes)) + { + ssize_t amount = read(fd, bytes + offset, sizeof(bytes) - offset); + if (amount <= 0) + { + close(fd); + return FALSE; + } + offset += (size_t)amount; + } + close(fd); + + bytes[6] = (uint8)((bytes[6] & 0x0f) | 0x40); + bytes[8] = (uint8)((bytes[8] & 0x3f) | 0x80); + snprintf( + output, + 37, + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-" + "%02x%02x%02x%02x%02x%02x", + bytes[0], bytes[1], bytes[2], bytes[3], + bytes[4], bytes[5], bytes[6], bytes[7], + bytes[8], bytes[9], bytes[10], bytes[11], + bytes[12], bytes[13], bytes[14], bytes[15]); + return TRUE; +} + +static char *Conversation_Store_Copy_Text( + const char *value, + Dowa_Arena *p_arena) +{ + const char *source = value ? value : ""; + size_t length = strlen(source); + char *copy = Dowa_Arena_Allocate(p_arena, length + 1); + if (!copy) + return NULL; + memcpy(copy, source, length + 1); + return copy; +} + +static boolean Conversation_Store_Exists_Locked( + Conversation_Store *p_store, + const char *conversation_id) +{ + Dowa_Arena *p_arena = Dowa_Arena_Create(1024); + if (!p_arena) + return FALSE; + const char *parameters[] = {conversation_id}; + Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( + p_store->p_connection, + "SELECT 1 FROM conversations WHERE id = ? AND status != 'deleted'", + 1, + parameters, + p_arena); + boolean exists = p_result && Deita_Result_Set_Next(p_result); + if (p_result) + Deita_Result_Set_Free(p_result); + Dowa_Arena_Free(p_arena); + return exists; +} + +static Conversation_Store_Result Conversation_Store_Rollback( + Conversation_Store *p_store, + Conversation_Store_Result result) +{ + Deita_Query_Execute_Update(p_store->p_connection, "ROLLBACK"); + return result; +} + +Conversation_Store *Conversation_Store_Create(const char *database_path) +{ + if (!database_path) + return NULL; + + Conversation_Store *p_store = calloc(1, sizeof(*p_store)); + if (!p_store) + return NULL; + p_store->p_connection = Deita_Connection_Create( + DEITA_DATABASE_TYPE_SQLITE3, + database_path); + if (!p_store->p_connection || + !Deita_Connection_Is_Open(p_store->p_connection)) + { + if (p_store->p_connection) + Deita_Connection_Close(p_store->p_connection); + free(p_store); + return NULL; + } + if (pthread_mutex_init(&p_store->mutex, NULL) != 0) + { + Deita_Connection_Close(p_store->p_connection); + free(p_store); + return NULL; + } + + const char *schema = + "PRAGMA foreign_keys = ON;" + "PRAGMA journal_mode = WAL;" + "CREATE TABLE IF NOT EXISTS conversations (" + "id TEXT PRIMARY KEY," + "copilot_session_id TEXT NOT NULL UNIQUE," + "title TEXT NOT NULL DEFAULT ''," + "status TEXT NOT NULL DEFAULT 'active'," + "created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," + "updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))" + ");" + "CREATE TABLE IF NOT EXISTS conversation_turns (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "conversation_id TEXT NOT NULL," + "sequence INTEGER NOT NULL," + "role TEXT NOT NULL," + "content TEXT NOT NULL DEFAULT ''," + "status TEXT NOT NULL," + "request_id TEXT," + "error_message TEXT," + "input_tokens INTEGER NOT NULL DEFAULT 0," + "output_tokens INTEGER NOT NULL DEFAULT 0," + "created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," + "completed_at INTEGER," + "FOREIGN KEY(conversation_id) REFERENCES conversations(id) " + "ON DELETE CASCADE," + "UNIQUE(conversation_id, sequence)" + ");" + "CREATE UNIQUE INDEX IF NOT EXISTS " + "idx_conversation_active_request " + "ON conversation_turns(conversation_id) " + "WHERE role = 'assistant' AND status = 'active';" + "CREATE INDEX IF NOT EXISTS idx_conversations_updated " + "ON conversations(updated_at DESC);" + "CREATE INDEX IF NOT EXISTS idx_turns_conversation_sequence " + "ON conversation_turns(conversation_id, sequence);"; + if (Deita_Query_Execute_Update(p_store->p_connection, schema) < 0) + { + Conversation_Store_Destroy(p_store); + return NULL; + } + if (Deita_Query_Execute_Update( + p_store->p_connection, + "UPDATE conversation_turns " + "SET status = 'failed', " + "error_message = 'Interrupted by server restart', " + "completed_at = strftime('%s','now') " + "WHERE role = 'assistant' AND status = 'active'") < 0) + { + Conversation_Store_Destroy(p_store); + return NULL; + } + return p_store; +} + +void Conversation_Store_Destroy(Conversation_Store *p_store) +{ + if (!p_store) + return; + if (p_store->p_connection) + Deita_Connection_Close(p_store->p_connection); + pthread_mutex_destroy(&p_store->mutex); + free(p_store); +} + +Conversation_Store_Result Conversation_Store_Create_Conversation( + Conversation_Store *p_store, + const char *title, + char output_id[37]) +{ + if (!p_store || !output_id || + !Conversation_Store_Generate_UUID(output_id)) + return CONVERSATION_STORE_ERROR; + + const char *parameters[] = { + output_id, + output_id, + title ? title : "", + }; + pthread_mutex_lock(&p_store->mutex); + int32 result = Deita_Query_Execute_Update_Prepared( + p_store->p_connection, + "INSERT INTO conversations (id, copilot_session_id, title) " + "VALUES (?, ?, ?)", + 3, + parameters); + pthread_mutex_unlock(&p_store->mutex); + return result < 0 ? CONVERSATION_STORE_ERROR : CONVERSATION_STORE_OK; +} + +Conversation_Store_Result Conversation_Store_Get( + Conversation_Store *p_store, + const char *conversation_id, + Conversation_Record *p_record, + Dowa_Arena *p_arena) +{ + if (!p_store || !conversation_id || !p_record || !p_arena) + return CONVERSATION_STORE_ERROR; + memset(p_record, 0, sizeof(*p_record)); + + const char *parameters[] = {conversation_id}; + pthread_mutex_lock(&p_store->mutex); + Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( + p_store->p_connection, + "SELECT id, copilot_session_id, title, status, created_at, updated_at " + "FROM conversations WHERE id = ? AND status != 'deleted'", + 1, + parameters, + p_arena); + if (!p_result || !Deita_Result_Set_Next(p_result)) + { + if (p_result) + Deita_Result_Set_Free(p_result); + pthread_mutex_unlock(&p_store->mutex); + return CONVERSATION_STORE_NOT_FOUND; + } + p_record->id = Conversation_Store_Copy_Text( + Deita_Result_Set_Get_Text(p_result, 0), p_arena); + p_record->copilot_session_id = Conversation_Store_Copy_Text( + Deita_Result_Set_Get_Text(p_result, 1), p_arena); + p_record->title = Conversation_Store_Copy_Text( + Deita_Result_Set_Get_Text(p_result, 2), p_arena); + p_record->status = Conversation_Store_Copy_Text( + Deita_Result_Set_Get_Text(p_result, 3), p_arena); + p_record->created_at = Deita_Result_Set_Get_Integer(p_result, 4); + p_record->updated_at = Deita_Result_Set_Get_Integer(p_result, 5); + Deita_Result_Set_Free(p_result); + + p_result = Deita_Query_Execute_Prepared( + p_store->p_connection, + "SELECT id, sequence, role, content, status, request_id, " + "error_message, input_tokens, output_tokens, created_at, completed_at " + "FROM (SELECT id, sequence, role, content, status, request_id, " + "error_message, input_tokens, output_tokens, created_at, completed_at " + "FROM conversation_turns WHERE conversation_id = ? " + "ORDER BY sequence DESC LIMIT 20) ORDER BY sequence", + 1, + parameters, + p_arena); + if (!p_result) + { + pthread_mutex_unlock(&p_store->mutex); + return CONVERSATION_STORE_ERROR; + } + while (p_result && Deita_Result_Set_Next(p_result)) + { + Conversation_Turn turn = {0}; + turn.id = Deita_Result_Set_Get_Integer(p_result, 0); + turn.sequence = Deita_Result_Set_Get_Integer(p_result, 1); + turn.role = Conversation_Store_Copy_Text( + Deita_Result_Set_Get_Text(p_result, 2), p_arena); + turn.content = Conversation_Store_Copy_Text( + Deita_Result_Set_Get_Text(p_result, 3), p_arena); + turn.status = Conversation_Store_Copy_Text( + Deita_Result_Set_Get_Text(p_result, 4), p_arena); + turn.request_id = Conversation_Store_Copy_Text( + Deita_Result_Set_Get_Text(p_result, 5), p_arena); + turn.error_message = Conversation_Store_Copy_Text( + Deita_Result_Set_Get_Text(p_result, 6), p_arena); + turn.input_tokens = Deita_Result_Set_Get_Integer(p_result, 7); + turn.output_tokens = Deita_Result_Set_Get_Integer(p_result, 8); + turn.created_at = Deita_Result_Set_Get_Integer(p_result, 9); + turn.completed_at = Deita_Result_Set_Get_Integer(p_result, 10); + Dowa_Array_Push_Arena(p_record->turns, turn, p_arena); + } + boolean turns_error = Deita_Result_Set_Has_Error(p_result); + if (p_result) + Deita_Result_Set_Free(p_result); + pthread_mutex_unlock(&p_store->mutex); + return turns_error ? CONVERSATION_STORE_ERROR : CONVERSATION_STORE_OK; +} + +Conversation_Store_Result Conversation_Store_Update_Title( + Conversation_Store *p_store, + const char *conversation_id, + const char *title) +{ + if (!p_store || !conversation_id || !title) + return CONVERSATION_STORE_ERROR; + const char *parameters[] = {title, conversation_id}; + pthread_mutex_lock(&p_store->mutex); + int32 result = Deita_Query_Execute_Update_Prepared( + p_store->p_connection, + "UPDATE conversations SET title = ?, " + "updated_at = strftime('%s','now') " + "WHERE id = ? AND status != 'deleted'", + 2, + parameters); + pthread_mutex_unlock(&p_store->mutex); + if (result < 0) + return CONVERSATION_STORE_ERROR; + return result == 0 ? CONVERSATION_STORE_NOT_FOUND : CONVERSATION_STORE_OK; +} + +Conversation_Store_Result Conversation_Store_Delete( + Conversation_Store *p_store, + const char *conversation_id) +{ + if (!p_store || !conversation_id) + return CONVERSATION_STORE_ERROR; + const char *parameters[] = {conversation_id}; + pthread_mutex_lock(&p_store->mutex); + int32 result = Deita_Query_Execute_Update_Prepared( + p_store->p_connection, + "DELETE FROM conversations WHERE id = ?", + 1, + parameters); + pthread_mutex_unlock(&p_store->mutex); + if (result < 0) + return CONVERSATION_STORE_ERROR; + return result == 0 ? CONVERSATION_STORE_NOT_FOUND : CONVERSATION_STORE_OK; +} + +Conversation_Store_Result Conversation_Store_Begin_Turn( + Conversation_Store *p_store, + const char *conversation_id, + const char *request_id, + const char *prompt) +{ + if (!p_store || !conversation_id || !request_id || !prompt) + return CONVERSATION_STORE_ERROR; + + pthread_mutex_lock(&p_store->mutex); + if (Deita_Query_Execute_Update( + p_store->p_connection, "BEGIN IMMEDIATE") < 0) + { + pthread_mutex_unlock(&p_store->mutex); + return CONVERSATION_STORE_ERROR; + } + if (!Conversation_Store_Exists_Locked(p_store, conversation_id)) + { + Conversation_Store_Rollback( + p_store, CONVERSATION_STORE_NOT_FOUND); + pthread_mutex_unlock(&p_store->mutex); + return CONVERSATION_STORE_NOT_FOUND; + } + + Dowa_Arena *p_arena = Dowa_Arena_Create(2048); + const char *parameters[] = {conversation_id}; + Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( + p_store->p_connection, + "SELECT COALESCE(MAX(sequence), 0), " + "SUM(CASE WHEN role = 'assistant' AND status = 'active' " + "THEN 1 ELSE 0 END) " + "FROM conversation_turns WHERE conversation_id = ?", + 1, + parameters, + p_arena); + if (!p_result || !Deita_Result_Set_Next(p_result)) + { + if (p_result) + Deita_Result_Set_Free(p_result); + Dowa_Arena_Free(p_arena); + Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); + pthread_mutex_unlock(&p_store->mutex); + return CONVERSATION_STORE_ERROR; + } + int64 next_sequence = Deita_Result_Set_Get_Integer(p_result, 0) + 1; + int64 active_count = Deita_Result_Set_Get_Integer(p_result, 1); + Deita_Result_Set_Free(p_result); + Dowa_Arena_Free(p_arena); + if (active_count > 0) + { + Conversation_Store_Rollback(p_store, CONVERSATION_STORE_CONFLICT); + pthread_mutex_unlock(&p_store->mutex); + return CONVERSATION_STORE_CONFLICT; + } + + char user_sequence[32]; + char assistant_sequence[32]; + snprintf(user_sequence, sizeof(user_sequence), "%lld", + (long long)next_sequence); + snprintf(assistant_sequence, sizeof(assistant_sequence), "%lld", + (long long)(next_sequence + 1)); + const char *user_parameters[] = { + conversation_id, user_sequence, prompt, request_id, + }; + if (Deita_Query_Execute_Update_Prepared( + p_store->p_connection, + "INSERT INTO conversation_turns " + "(conversation_id, sequence, role, content, status, request_id, " + "completed_at) VALUES (?, ?, 'user', ?, 'complete', ?, " + "strftime('%s','now'))", + 4, + user_parameters) < 0) + { + Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); + pthread_mutex_unlock(&p_store->mutex); + return CONVERSATION_STORE_ERROR; + } + const char *assistant_parameters[] = { + conversation_id, assistant_sequence, request_id, + }; + if (Deita_Query_Execute_Update_Prepared( + p_store->p_connection, + "INSERT INTO conversation_turns " + "(conversation_id, sequence, role, status, request_id) " + "VALUES (?, ?, 'assistant', 'active', ?)", + 3, + assistant_parameters) < 0 || + Deita_Query_Execute_Update_Prepared( + p_store->p_connection, + "UPDATE conversations SET updated_at = strftime('%s','now') " + "WHERE id = ?", + 1, + parameters) < 0 || + Deita_Query_Execute_Update( + p_store->p_connection, "COMMIT") < 0) + { + Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); + pthread_mutex_unlock(&p_store->mutex); + return CONVERSATION_STORE_ERROR; + } + pthread_mutex_unlock(&p_store->mutex); + return CONVERSATION_STORE_OK; +} + +Conversation_Store_Result Conversation_Store_Complete_Turn( + Conversation_Store *p_store, + const char *conversation_id, + const char *request_id, + const char *content, + int64 input_tokens, + int64 output_tokens) +{ + if (!p_store || !conversation_id || !request_id || !content) + return CONVERSATION_STORE_ERROR; + char input_value[32]; + char output_value[32]; + snprintf(input_value, sizeof(input_value), "%lld", (long long)input_tokens); + snprintf(output_value, sizeof(output_value), "%lld", (long long)output_tokens); + const char *parameters[] = { + content, input_value, output_value, conversation_id, request_id, + }; + pthread_mutex_lock(&p_store->mutex); + int32 result = Deita_Query_Execute_Update_Prepared( + p_store->p_connection, + "UPDATE conversation_turns SET content = ?, status = 'complete', " + "input_tokens = ?, output_tokens = ?, " + "completed_at = strftime('%s','now') " + "WHERE conversation_id = ? AND request_id = ? " + "AND role = 'assistant' AND status = 'active'", + 5, + parameters); + pthread_mutex_unlock(&p_store->mutex); + if (result < 0) + return CONVERSATION_STORE_ERROR; + return result == 0 ? CONVERSATION_STORE_NOT_FOUND : CONVERSATION_STORE_OK; +} + +Conversation_Store_Result Conversation_Store_Fail_Turn( + Conversation_Store *p_store, + const char *conversation_id, + const char *request_id, + const char *error_message, + boolean aborted) +{ + if (!p_store || !conversation_id || !request_id) + return CONVERSATION_STORE_ERROR; + const char *parameters[] = { + aborted ? "aborted" : "failed", + error_message ? error_message : "", + conversation_id, + request_id, + }; + pthread_mutex_lock(&p_store->mutex); + int32 result = Deita_Query_Execute_Update_Prepared( + p_store->p_connection, + "UPDATE conversation_turns SET status = ?, error_message = ?, " + "completed_at = strftime('%s','now') " + "WHERE conversation_id = ? AND request_id = ? " + "AND role = 'assistant' AND status = 'active'", + 4, + parameters); + pthread_mutex_unlock(&p_store->mutex); + if (result < 0) + return CONVERSATION_STORE_ERROR; + return result == 0 ? CONVERSATION_STORE_NOT_FOUND : CONVERSATION_STORE_OK; +}