Mercurial
view mrjunejune/conversation_store.c @ 279:b3b547563ec7
Add Google connector service and agent wiki
Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:22:36 -0700 |
| parents | 04fee26ecce0 |
| children |
line wrap: on
line source
#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; } /* ------------------------------------------------------------------ */ /* Schema migration helpers (called from Conversation_Store_Create) */ /* ------------------------------------------------------------------ */ static const char *Conversation_Store_Owner_Kind_String( Conversation_Owner_Kind kind) { switch (kind) { case CONVERSATION_OWNER_KIND_USER: return "user"; case CONVERSATION_OWNER_KIND_GUEST: return "guest"; case CONVERSATION_OWNER_KIND_LEGACY: return "legacy"; default: return "legacy"; } } /* Returns TRUE if column exists in table (mutex must NOT be held). */ static boolean Conversation_Store_Column_Exists( Conversation_Store *p_store, const char *table_name, const char *column_name) { char sql[256]; snprintf(sql, sizeof(sql), "SELECT 1 FROM pragma_table_info('%s') WHERE name = '%s'", table_name, column_name); Dowa_Arena *p_arena = Dowa_Arena_Create(512); if (!p_arena) return FALSE; Deita_Result_Set *p_result = Deita_Query_Execute_Prepared(p_store->p_connection, sql, 0, NULL, 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; } /* * Migration 1: add owner_kind / owner_id columns and listing index. * Existing rows become owner_kind='legacy', owner_id=NULL. * Safe to call on a database that was created by new code (idempotent). */ static boolean Conversation_Store_Apply_Migration_1( Conversation_Store *p_store) { /* Check migrations ledger */ Dowa_Arena *p_arena = Dowa_Arena_Create(512); if (!p_arena) return FALSE; Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( p_store->p_connection, "SELECT 1 FROM conversation_schema_migrations WHERE version = 1", 0, NULL, p_arena); boolean already_done = p_result && Deita_Result_Set_Next(p_result); if (p_result) Deita_Result_Set_Free(p_result); Dowa_Arena_Free(p_arena); if (already_done) return TRUE; if (!Conversation_Store_Column_Exists(p_store, "conversations", "owner_kind")) { if (Deita_Query_Execute_Update( p_store->p_connection, "ALTER TABLE conversations ADD COLUMN owner_kind TEXT " "NOT NULL DEFAULT 'legacy'") < 0) return FALSE; } if (!Conversation_Store_Column_Exists(p_store, "conversations", "owner_id")) { if (Deita_Query_Execute_Update( p_store->p_connection, "ALTER TABLE conversations ADD COLUMN owner_id TEXT") < 0) return FALSE; } if (Deita_Query_Execute_Update( p_store->p_connection, "CREATE INDEX IF NOT EXISTS idx_conversations_owner_listing " "ON conversations(owner_kind, owner_id, updated_at DESC, id)") < 0) return FALSE; if (Deita_Query_Execute_Update( p_store->p_connection, "INSERT OR IGNORE INTO conversation_schema_migrations (version) " "VALUES (1)") < 0) return FALSE; return TRUE; } /* * Migration 2: create the guest-to-user transfer mapping table. * Records a permanent mapping from guest_id → user_id set at transfer time. * Used by Create_Owned to redirect stale guest creates to the mapped user. */ static boolean Conversation_Store_Apply_Migration_2( Conversation_Store *p_store) { Dowa_Arena *p_arena = Dowa_Arena_Create(512); if (!p_arena) return FALSE; Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( p_store->p_connection, "SELECT 1 FROM conversation_schema_migrations WHERE version = 2", 0, NULL, p_arena); boolean already_done = p_result && Deita_Result_Set_Next(p_result); if (p_result) Deita_Result_Set_Free(p_result); Dowa_Arena_Free(p_arena); if (already_done) return TRUE; if (Deita_Query_Execute_Update( p_store->p_connection, "CREATE TABLE IF NOT EXISTS conversation_guest_transfers (" "guest_id TEXT PRIMARY KEY," "user_id TEXT NOT NULL," "transferred_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))" ")") < 0) return FALSE; if (Deita_Query_Execute_Update( p_store->p_connection, "INSERT OR IGNORE INTO conversation_schema_migrations (version) " "VALUES (2)") < 0) return FALSE; return TRUE; } /* * Migration 3: recreate the owner listing index with id DESC so that * keyset pagination is stable when updated_at values collide. * Drops and recreates the index atomically from the migration ledger's * perspective; safe to run on any database that has migration 1 applied. */ static boolean Conversation_Store_Apply_Migration_3( Conversation_Store *p_store) { Dowa_Arena *p_arena = Dowa_Arena_Create(512); if (!p_arena) return FALSE; Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( p_store->p_connection, "SELECT 1 FROM conversation_schema_migrations WHERE version = 3", 0, NULL, p_arena); boolean already_done = p_result && Deita_Result_Set_Next(p_result); if (p_result) Deita_Result_Set_Free(p_result); Dowa_Arena_Free(p_arena); if (already_done) return TRUE; if (Deita_Query_Execute_Update( p_store->p_connection, "DROP INDEX IF EXISTS idx_conversations_owner_listing") < 0) return FALSE; if (Deita_Query_Execute_Update( p_store->p_connection, "CREATE INDEX IF NOT EXISTS idx_conversations_owner_listing " "ON conversations(owner_kind, owner_id, updated_at DESC, id DESC)") < 0) return FALSE; if (Deita_Query_Execute_Update( p_store->p_connection, "INSERT OR IGNORE INTO conversation_schema_migrations (version) " "VALUES (3)") < 0) return FALSE; return TRUE; } 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);" "CREATE TABLE IF NOT EXISTS conversation_schema_migrations (" "version INTEGER PRIMARY KEY," "applied_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))" ");"; if (Deita_Query_Execute_Update(p_store->p_connection, schema) < 0) { Conversation_Store_Destroy(p_store); return NULL; } if (!Conversation_Store_Apply_Migration_1(p_store)) { Conversation_Store_Destroy(p_store); return NULL; } if (!Conversation_Store_Apply_Migration_2(p_store)) { Conversation_Store_Destroy(p_store); return NULL; } if (!Conversation_Store_Apply_Migration_3(p_store)) { 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; } /* ------------------------------------------------------------------ */ /* Owner-aware APIs */ /* ------------------------------------------------------------------ */ Conversation_Store_Result Conversation_Store_Create_Owned( Conversation_Store *p_store, const char *title, const Conversation_Owner *p_owner, char output_id[37]) { if (!p_store || !p_owner || !output_id) return CONVERSATION_STORE_ERROR; if (p_owner->kind == CONVERSATION_OWNER_KIND_LEGACY || p_owner->id[0] == '\0') return CONVERSATION_STORE_ERROR; if (!Conversation_Store_Generate_UUID(output_id)) return CONVERSATION_STORE_ERROR; /* Resolved owner fields — may be overridden by transfer mapping below */ const char *resolved_kind = Conversation_Store_Owner_Kind_String(p_owner->kind); char resolved_id[37]; strncpy(resolved_id, p_owner->id, 36); resolved_id[36] = '\0'; 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; } /* For guest owners: check transfer mapping within the same transaction. * If the guest was transferred to a user, assign to that user instead. */ if (p_owner->kind == CONVERSATION_OWNER_KIND_GUEST) { Dowa_Arena *p_arena = Dowa_Arena_Create(512); if (!p_arena) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } const char *check_params[] = {p_owner->id}; Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( p_store->p_connection, "SELECT user_id FROM conversation_guest_transfers WHERE guest_id = ?", 1, check_params, p_arena); if (p_result && Deita_Result_Set_Next(p_result)) { const char *mapped_user = Deita_Result_Set_Get_Text(p_result, 0); if (mapped_user && mapped_user[0] != '\0') { strncpy(resolved_id, mapped_user, 36); resolved_id[36] = '\0'; resolved_kind = "user"; } } if (p_result) Deita_Result_Set_Free(p_result); Dowa_Arena_Free(p_arena); } const char *parameters[] = { output_id, output_id, title ? title : "", resolved_kind, resolved_id, }; int32 result = Deita_Query_Execute_Update_Prepared( p_store->p_connection, "INSERT INTO conversations " "(id, copilot_session_id, title, owner_kind, owner_id) " "VALUES (?, ?, ?, ?, ?)", 5, parameters); if (result < 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 result < 0 ? CONVERSATION_STORE_ERROR : CONVERSATION_STORE_OK; } Conversation_Store_Result Conversation_Store_Get_Owned( Conversation_Store *p_store, const char *conversation_id, const Conversation_Owner *p_owner, Conversation_Record *p_record, Dowa_Arena *p_arena) { if (!p_store || !conversation_id || !p_owner || !p_record || !p_arena) return CONVERSATION_STORE_ERROR; if (p_owner->kind == CONVERSATION_OWNER_KIND_LEGACY || p_owner->id[0] == '\0') return CONVERSATION_STORE_ERROR; memset(p_record, 0, sizeof(*p_record)); const char *kind_str = Conversation_Store_Owner_Kind_String(p_owner->kind); const char *parameters[] = {conversation_id, kind_str, p_owner->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' " "AND owner_kind = ? AND owner_id = ?", 3, 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); const char *turn_params[] = {conversation_id}; 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, turn_params, p_arena); if (!p_result) { pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } while (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); 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_Owned( Conversation_Store *p_store, const char *conversation_id, const Conversation_Owner *p_owner, const char *title) { if (!p_store || !conversation_id || !p_owner || !title) return CONVERSATION_STORE_ERROR; if (p_owner->kind == CONVERSATION_OWNER_KIND_LEGACY || p_owner->id[0] == '\0') return CONVERSATION_STORE_ERROR; const char *kind_str = Conversation_Store_Owner_Kind_String(p_owner->kind); const char *parameters[] = {title, conversation_id, kind_str, p_owner->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' " "AND owner_kind = ? AND owner_id = ?", 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; } Conversation_Store_Result Conversation_Store_Delete_Owned( Conversation_Store *p_store, const char *conversation_id, const Conversation_Owner *p_owner) { if (!p_store || !conversation_id || !p_owner) return CONVERSATION_STORE_ERROR; if (p_owner->kind == CONVERSATION_OWNER_KIND_LEGACY || p_owner->id[0] == '\0') return CONVERSATION_STORE_ERROR; const char *kind_str = Conversation_Store_Owner_Kind_String(p_owner->kind); const char *parameters[] = {conversation_id, kind_str, p_owner->id}; pthread_mutex_lock(&p_store->mutex); int32 result = Deita_Query_Execute_Update_Prepared( p_store->p_connection, "DELETE FROM conversations WHERE id = ? " "AND owner_kind = ? AND owner_id = ?", 3, parameters); pthread_mutex_unlock(&p_store->mutex); if (result < 0) return CONVERSATION_STORE_ERROR; return result == 0 ? CONVERSATION_STORE_NOT_FOUND : CONVERSATION_STORE_OK; } /* Checks conversation ownership without loading turns (called under mutex). */ static boolean Conversation_Store_Owns_Locked( Conversation_Store *p_store, const char *conversation_id, const Conversation_Owner *p_owner) { const char *kind_str = Conversation_Store_Owner_Kind_String(p_owner->kind); const char *parameters[] = {conversation_id, kind_str, p_owner->id}; Dowa_Arena *p_arena = Dowa_Arena_Create(1024); if (!p_arena) return FALSE; Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( p_store->p_connection, "SELECT 1 FROM conversations " "WHERE id = ? AND status != 'deleted' " "AND owner_kind = ? AND owner_id = ?", 3, parameters, p_arena); boolean owns = p_result && Deita_Result_Set_Next(p_result); if (p_result) Deita_Result_Set_Free(p_result); Dowa_Arena_Free(p_arena); return owns; } Conversation_Store_Result Conversation_Store_Begin_Turn_Owned( Conversation_Store *p_store, const char *conversation_id, const Conversation_Owner *p_owner, const char *request_id, const char *prompt) { if (!p_store || !conversation_id || !p_owner || !request_id || !prompt) return CONVERSATION_STORE_ERROR; if (p_owner->kind == CONVERSATION_OWNER_KIND_LEGACY || p_owner->id[0] == '\0') 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_Owns_Locked(p_store, conversation_id, p_owner)) { 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 *seq_params[] = {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, seq_params, 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_params[] = { 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_params) < 0) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } const char *asst_params[] = {conversation_id, assistant_sequence, request_id}; const char *conv_params[] = {conversation_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, asst_params) < 0 || Deita_Query_Execute_Update_Prepared( p_store->p_connection, "UPDATE conversations SET updated_at = strftime('%s','now') " "WHERE id = ?", 1, conv_params) < 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_List( Conversation_Store *p_store, const Conversation_Owner *p_owner, int64 cursor_updated_at, const char *cursor_id, int32 limit, Conversation_Summary **pp_summaries, int32 *p_count, Dowa_Arena *p_arena) { if (!p_store || !p_owner || !pp_summaries || !p_count || !p_arena) return CONVERSATION_STORE_ERROR; *pp_summaries = NULL; *p_count = 0; /* Legacy conversations are never listed */ if (p_owner->kind == CONVERSATION_OWNER_KIND_LEGACY || p_owner->id[0] == '\0') return CONVERSATION_STORE_OK; if (limit < 1) limit = 1; if (limit > 50) limit = 50; const char *kind_str = Conversation_Store_Owner_Kind_String(p_owner->kind); char limit_str[16]; snprintf(limit_str, sizeof(limit_str), "%d", limit); Deita_Result_Set *p_result; pthread_mutex_lock(&p_store->mutex); if (cursor_updated_at > 0 && cursor_id && cursor_id[0] != '\0') { char ts_str[32]; snprintf(ts_str, sizeof(ts_str), "%lld", (long long)cursor_updated_at); const char *params[] = { kind_str, p_owner->id, ts_str, ts_str, cursor_id, limit_str, }; p_result = Deita_Query_Execute_Prepared( p_store->p_connection, "SELECT id, title, status, created_at, updated_at, " "(SELECT COUNT(*) FROM conversation_turns " " WHERE conversation_id = conversations.id) AS turn_count, " "(SELECT SUBSTR(content, 1, 201) FROM conversation_turns " " WHERE conversation_id = conversations.id " " ORDER BY sequence DESC LIMIT 1) AS last_msg " "FROM conversations " "WHERE owner_kind = ? AND owner_id = ? AND status != 'deleted' " "AND (updated_at < ? OR (updated_at = ? AND id < ?)) " "ORDER BY updated_at DESC, id DESC LIMIT ?", 6, params, p_arena); } else { const char *params[] = {kind_str, p_owner->id, limit_str}; p_result = Deita_Query_Execute_Prepared( p_store->p_connection, "SELECT id, title, status, created_at, updated_at, " "(SELECT COUNT(*) FROM conversation_turns " " WHERE conversation_id = conversations.id) AS turn_count, " "(SELECT SUBSTR(content, 1, 201) FROM conversation_turns " " WHERE conversation_id = conversations.id " " ORDER BY sequence DESC LIMIT 1) AS last_msg " "FROM conversations " "WHERE owner_kind = ? AND owner_id = ? AND status != 'deleted' " "ORDER BY updated_at DESC, id DESC LIMIT ?", 3, params, p_arena); } if (!p_result) { pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } Conversation_Summary *summaries = NULL; while (Deita_Result_Set_Next(p_result)) { Conversation_Summary s = {0}; s.id = Conversation_Store_Copy_Text(Deita_Result_Set_Get_Text(p_result, 0), p_arena); s.title = Conversation_Store_Copy_Text(Deita_Result_Set_Get_Text(p_result, 1), p_arena); s.status = Conversation_Store_Copy_Text(Deita_Result_Set_Get_Text(p_result, 2), p_arena); s.created_at = Deita_Result_Set_Get_Integer(p_result, 3); s.updated_at = Deita_Result_Set_Get_Integer(p_result, 4); s.turn_count = Deita_Result_Set_Get_Integer(p_result, 5); const char *last_msg_raw = Deita_Result_Set_Get_Text(p_result, 6); if (last_msg_raw && last_msg_raw[0] != '\0') { /* Truncate to 200 chars maximum */ size_t msg_len = strlen(last_msg_raw); if (msg_len > 200) msg_len = 200; char *preview = Dowa_Arena_Allocate(p_arena, msg_len + 1); if (preview) { memcpy(preview, last_msg_raw, msg_len); preview[msg_len] = '\0'; } s.last_message_preview = preview; } else { s.last_message_preview = Conversation_Store_Copy_Text("", p_arena); } Dowa_Array_Push_Arena(summaries, s, p_arena); } boolean has_error = Deita_Result_Set_Has_Error(p_result); Deita_Result_Set_Free(p_result); pthread_mutex_unlock(&p_store->mutex); if (has_error) return CONVERSATION_STORE_ERROR; *pp_summaries = summaries; *p_count = (int32)Dowa_Array_Length(summaries); return CONVERSATION_STORE_OK; } Conversation_Store_Result Conversation_Store_Claim_Legacy( Conversation_Store *p_store, const char *conversation_id, const char *user_id) { if (!p_store || !conversation_id || !user_id || user_id[0] == '\0') 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; } Dowa_Arena *p_arena = Dowa_Arena_Create(512); if (!p_arena) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } const char *check_params[] = {conversation_id}; Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( p_store->p_connection, "SELECT owner_kind FROM conversations " "WHERE id = ? AND status != 'deleted'", 1, check_params, p_arena); boolean found = p_result && Deita_Result_Set_Next(p_result); const char *existing_kind = found ? Deita_Result_Set_Get_Text(p_result, 0) : NULL; boolean is_claimable = existing_kind && strcmp(existing_kind, "legacy") == 0; if (p_result) Deita_Result_Set_Free(p_result); Dowa_Arena_Free(p_arena); if (!found) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_NOT_FOUND); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_NOT_FOUND; } if (!is_claimable) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_CONFLICT); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_CONFLICT; } const char *update_params[] = {user_id, conversation_id}; int32 updated = Deita_Query_Execute_Update_Prepared( p_store->p_connection, "UPDATE conversations SET owner_kind = 'user', owner_id = ?, " "updated_at = strftime('%s','now') " "WHERE id = ? AND owner_kind = 'legacy'", 2, update_params); if (updated < 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 updated > 0 ? CONVERSATION_STORE_OK : CONVERSATION_STORE_NOT_FOUND; } Conversation_Store_Result Conversation_Store_Transfer_Guest_To_User( Conversation_Store *p_store, const char *guest_id, const char *user_id) { if (!p_store || !guest_id || !user_id || guest_id[0] == '\0' || user_id[0] == '\0') 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; } /* Check for an existing mapping for this guest_id */ Dowa_Arena *p_arena = Dowa_Arena_Create(512); if (!p_arena) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } const char *check_params[] = {guest_id}; Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( p_store->p_connection, "SELECT user_id FROM conversation_guest_transfers WHERE guest_id = ?", 1, check_params, p_arena); boolean mapping_exists = p_result && Deita_Result_Set_Next(p_result); const char *existing_user = mapping_exists ? Deita_Result_Set_Get_Text(p_result, 0) : NULL; boolean same_user = mapping_exists && existing_user && strcmp(existing_user, user_id) == 0; boolean conflict = mapping_exists && !same_user; if (p_result) Deita_Result_Set_Free(p_result); Dowa_Arena_Free(p_arena); if (conflict) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_CONFLICT); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_CONFLICT; } if (!mapping_exists) { /* Record the mapping so future Create_Owned for this guest uses user */ const char *map_params[] = {guest_id, user_id}; if (Deita_Query_Execute_Update_Prepared( p_store->p_connection, "INSERT OR IGNORE INTO conversation_guest_transfers " "(guest_id, user_id) VALUES (?, ?)", 2, map_params) < 0) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } /* Transfer existing conversations from guest to user */ const char *transfer_params[] = {user_id, guest_id}; if (Deita_Query_Execute_Update_Prepared( p_store->p_connection, "UPDATE conversations SET owner_kind = 'user', owner_id = ?, " "updated_at = strftime('%s','now') " "WHERE owner_kind = 'guest' AND owner_id = ?", 2, transfer_params) < 0) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } } /* same_user mapping already exists: idempotent no-op */ if (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_Transfer_Guest_To_User_Atomic( Conversation_Store *p_store, const char *guest_id, const char *user_id) { if (!p_store || !guest_id || !user_id || guest_id[0] == '\0' || user_id[0] == '\0') 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; } /* Check for an existing mapping for this guest_id. */ Dowa_Arena *p_arena = Dowa_Arena_Create(512); if (!p_arena) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } const char *check_params[] = {guest_id}; Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( p_store->p_connection, "SELECT user_id FROM conversation_guest_transfers WHERE guest_id = ?", 1, check_params, p_arena); boolean mapping_exists = p_result && Deita_Result_Set_Next(p_result); const char *existing_user = mapping_exists ? Deita_Result_Set_Get_Text(p_result, 0) : NULL; boolean same_user = mapping_exists && existing_user && strcmp(existing_user, user_id) == 0; boolean conflict = mapping_exists && !same_user; if (p_result) Deita_Result_Set_Free(p_result); Dowa_Arena_Free(p_arena); if (conflict) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_CONFLICT); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_CONFLICT; } if (!same_user) { const char *map_params[] = {guest_id, user_id}; if (Deita_Query_Execute_Update_Prepared( p_store->p_connection, "INSERT OR IGNORE INTO conversation_guest_transfers " "(guest_id, user_id) VALUES (?, ?)", 2, map_params) < 0) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } const char *transfer_params[] = {user_id, guest_id}; if (Deita_Query_Execute_Update_Prepared( p_store->p_connection, "UPDATE conversations SET owner_kind = 'user', owner_id = ?, " "updated_at = strftime('%s','now') " "WHERE owner_kind = 'guest' AND owner_id = ?", 2, transfer_params) < 0) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } } /* * Quota cleanup — auth tables reside in the same SQLite file so the write * lock already held by this transaction covers them too. Decrement * output_tokens_reserved and delete reservation rows for this guest. */ const char *quota_upd_params[] = {guest_id, guest_id}; if (Deita_Query_Execute_Update_Prepared( p_store->p_connection, "UPDATE guest_usage" " SET output_tokens_reserved = MAX(0, output_tokens_reserved - (" " SELECT COALESCE(SUM(r.output_tokens_reserved), 0)" " FROM guest_usage_reservations r" " WHERE r.guest_id = ? AND r.window_start = guest_usage.window_start" " ))" " WHERE guest_id = ?", 2, quota_upd_params) < 0) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } const char *quota_del_params[] = {guest_id}; if (Deita_Query_Execute_Update_Prepared( p_store->p_connection, "DELETE FROM guest_usage_reservations WHERE guest_id = ?", 1, quota_del_params) < 0) { Conversation_Store_Rollback(p_store, CONVERSATION_STORE_ERROR); pthread_mutex_unlock(&p_store->mutex); return CONVERSATION_STORE_ERROR; } if (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; }