// retoor #include "db.h" #include "r_config.h" #include #include #include #include #include struct db_t { sqlite3 *conn; char *path; }; static char *expand_home_directory(const char *path) { if (!path) return NULL; if (path[0] != '~') return strdup(path); const char *home_dir = getenv("HOME"); if (!home_dir) home_dir = getenv("USERPROFILE"); if (!home_dir) return strdup(path); size_t home_len = strlen(home_dir); size_t path_len = strlen(path); char *expanded = malloc(home_len + path_len + 1); if (!expanded) return NULL; memcpy(expanded, home_dir, home_len); memcpy(expanded + home_len, path + 1, path_len); return expanded; } db_handle db_open(const char *path) { struct db_t *db = calloc(1, sizeof(struct db_t)); if (!db) return NULL; if (!path) { r_config_handle cfg = r_config_get_instance(); path = r_config_get_db_path(cfg); } db->path = expand_home_directory(path); if (!db->path) { free(db); return NULL; } if (sqlite3_open(db->path, &db->conn) != SQLITE_OK) { free(db->path); free(db); return NULL; } db_init(db); return db; } void db_close(db_handle db) { if (!db) return; if (db->conn) sqlite3_close(db->conn); free(db->path); free(db); } r_status_t db_init(db_handle db) { if (!db || !db->conn) return R_ERROR_INVALID_ARG; const char *sql = "CREATE TABLE IF NOT EXISTS kv (" " key TEXT PRIMARY KEY," " value TEXT NOT NULL," " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP," " updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" ");" "CREATE TABLE IF NOT EXISTS file_versions (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " path TEXT NOT NULL," " content TEXT NOT NULL," " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" ");" "CREATE TABLE IF NOT EXISTS conversations (" " session_key TEXT PRIMARY KEY," " data TEXT NOT NULL," " updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" ");" "CREATE TABLE IF NOT EXISTS blackboard (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " sender TEXT NOT NULL," " recipient TEXT," " topic TEXT NOT NULL," " message TEXT NOT NULL," " status TEXT DEFAULT 'pending'," " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP," " updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" ");" "CREATE TABLE IF NOT EXISTS gtr (" " task_hash TEXT PRIMARY KEY," " task_description TEXT NOT NULL," " status TEXT NOT NULL," " owner_agent TEXT NOT NULL," " result_summary TEXT," " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP," " updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" ");" "CREATE TABLE IF NOT EXISTS audit_log (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP," " issuer_agent TEXT NOT NULL," " recipient_agent TEXT NOT NULL," " order_description TEXT NOT NULL," " logic_justification TEXT NOT NULL," " status TEXT NOT NULL" ");" "CREATE TABLE IF NOT EXISTS agents (" " agent_id TEXT PRIMARY KEY," " role TEXT NOT NULL," " manager_id TEXT," " department TEXT," " budget_limit_tokens INTEGER DEFAULT 1000000," " used_tokens INTEGER DEFAULT 0," " last_heartbeat TIMESTAMP DEFAULT CURRENT_TIMESTAMP," " status TEXT DEFAULT 'active'" ");" "CREATE TABLE IF NOT EXISTS research_tasks (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " url_hash TEXT UNIQUE," " url TEXT NOT NULL," " status TEXT NOT NULL," " summary TEXT," " batch_id TEXT," " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP," " updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" ");" "CREATE TABLE IF NOT EXISTS snapshots (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " session_id TEXT NOT NULL," " description TEXT," " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" ");" "CREATE TABLE IF NOT EXISTS snapshot_files (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " snapshot_id INTEGER NOT NULL," " path TEXT NOT NULL," " content TEXT NOT NULL," " FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE" ");" "CREATE UNIQUE INDEX IF NOT EXISTS idx_snapshot_files_unique " "ON snapshot_files(snapshot_id, path);"; char *err_msg = NULL; if (sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg) != SQLITE_OK) { sqlite3_free(err_msg); return R_ERROR_DB_QUERY; } return R_SUCCESS; } r_status_t db_kv_set(db_handle db, const char *key, const char *value) { if (!db || !db->conn || !key || !value) return R_ERROR_INVALID_ARG; char *sql = sqlite3_mprintf( "INSERT OR REPLACE INTO kv (key, value, updated_at) VALUES (%Q, %Q, CURRENT_TIMESTAMP)", key, value); if (!sql) return R_ERROR_OUT_OF_MEMORY; char *err_msg = NULL; int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg); sqlite3_free(sql); if (rc != SQLITE_OK) { sqlite3_free(err_msg); return R_ERROR_DB_QUERY; } return R_SUCCESS; } r_status_t db_kv_get(db_handle db, const char *key, char **value) { if (!db || !db->conn || !key || !value) return R_ERROR_INVALID_ARG; *value = NULL; const char *sql = "SELECT value FROM kv WHERE key = ?"; sqlite3_stmt *stmt = NULL; if (sqlite3_prepare_v2(db->conn, sql, -1, &stmt, NULL) != SQLITE_OK) { return R_ERROR_DB_QUERY; } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); int rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char *val = (const char *)sqlite3_column_text(stmt, 0); *value = val ? strdup(val) : NULL; } sqlite3_finalize(stmt); return (rc == SQLITE_ROW) ? R_SUCCESS : R_ERROR_DB_NOT_FOUND; } r_status_t db_execute(db_handle db, const char *sql, struct json_object **result) { if (!db || !db->conn || !sql || !result) return R_ERROR_INVALID_ARG; *result = NULL; const char *select_check = sql; while (*select_check == ' ') select_check++; if (strncasecmp(select_check, "SELECT", 6) != 0) { char *err_msg = NULL; int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg); if (rc != SQLITE_OK) { struct json_object *error_obj = json_object_new_object(); json_object_object_add(error_obj, "error", json_object_new_string(err_msg ? err_msg : "Query failed")); sqlite3_free(err_msg); *result = error_obj; return R_ERROR_DB_QUERY; } struct json_object *success_obj = json_object_new_object(); json_object_object_add(success_obj, "success", json_object_new_boolean(1)); *result = success_obj; return R_SUCCESS; } sqlite3_stmt *stmt = NULL; if (sqlite3_prepare_v2(db->conn, sql, -1, &stmt, NULL) != SQLITE_OK) { struct json_object *error_obj = json_object_new_object(); json_object_object_add(error_obj, "error", json_object_new_string(sqlite3_errmsg(db->conn))); *result = error_obj; return R_ERROR_DB_QUERY; } struct json_object *array = json_object_new_array(); int col_count = sqlite3_column_count(stmt); while (sqlite3_step(stmt) == SQLITE_ROW) { struct json_object *row = json_object_new_object(); for (int i = 0; i < col_count; i++) { const char *col_name = sqlite3_column_name(stmt, i); int col_type = sqlite3_column_type(stmt, i); switch (col_type) { case SQLITE_INTEGER: json_object_object_add(row, col_name, json_object_new_int64(sqlite3_column_int64(stmt, i))); break; case SQLITE_FLOAT: json_object_object_add(row, col_name, json_object_new_double(sqlite3_column_double(stmt, i))); break; case SQLITE_TEXT: json_object_object_add(row, col_name, json_object_new_string((const char *)sqlite3_column_text(stmt, i))); break; case SQLITE_NULL: json_object_object_add(row, col_name, NULL); break; default: json_object_object_add(row, col_name, json_object_new_string((const char *)sqlite3_column_text(stmt, i))); break; } } json_object_array_add(array, row); } sqlite3_finalize(stmt); *result = array; return R_SUCCESS; } char *db_get_schema(db_handle db) { if (!db || !db->conn) return strdup("Database not available"); struct json_object *result = NULL; r_status_t status = db_execute(db, "SELECT name, sql FROM sqlite_master WHERE type='table' ORDER BY name", &result); if (status != R_SUCCESS || !result) { return strdup("Failed to get schema"); } char *schema = strdup(json_object_to_json_string_ext(result, JSON_C_TO_STRING_PRETTY)); json_object_put(result); return schema; } r_status_t db_store_file_version(db_handle db, const char *path) { if (!db || !db->conn || !path) return R_ERROR_INVALID_ARG; FILE *fp = fopen(path, "r"); if (!fp) return R_ERROR_FILE_NOT_FOUND; fseek(fp, 0, SEEK_END); long size = ftell(fp); rewind(fp); if (size <= 0 || size > 1000000) { fclose(fp); return R_SUCCESS; } char *content = malloc(size + 1); if (!content) { fclose(fp); return R_ERROR_OUT_OF_MEMORY; } size_t read_size = fread(content, 1, size, fp); content[read_size] = '\0'; fclose(fp); char *sql = sqlite3_mprintf( "INSERT INTO file_versions (path, content) VALUES (%Q, %Q)", path, content); free(content); if (!sql) return R_ERROR_OUT_OF_MEMORY; char *err_msg = NULL; int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg); sqlite3_free(sql); sqlite3_free(err_msg); return (rc == SQLITE_OK) ? R_SUCCESS : R_ERROR_DB_QUERY; } r_status_t db_save_conversation(db_handle db, const char *session_key, const char *data) { if (!db || !db->conn || !session_key || !data) return R_ERROR_INVALID_ARG; char *sql = sqlite3_mprintf( "INSERT OR REPLACE INTO conversations (session_key, data, updated_at) " "VALUES (%Q, %Q, CURRENT_TIMESTAMP)", session_key, data); if (!sql) return R_ERROR_OUT_OF_MEMORY; char *err_msg = NULL; int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg); sqlite3_free(sql); sqlite3_free(err_msg); return (rc == SQLITE_OK) ? R_SUCCESS : R_ERROR_DB_QUERY; } long long db_get_conversation_age(db_handle db, const char *session_key) { if (!db || !db->conn || !session_key) return -1; const char *sql = "SELECT CAST((julianday('now') - julianday(updated_at)) * 86400 AS INTEGER) FROM conversations WHERE session_key = ?"; sqlite3_stmt *stmt = NULL; if (sqlite3_prepare_v2(db->conn, sql, -1, &stmt, NULL) != SQLITE_OK) { return -1; } sqlite3_bind_text(stmt, 1, session_key, -1, SQLITE_STATIC); long long age = -1; if (sqlite3_step(stmt) == SQLITE_ROW) { age = sqlite3_column_int64(stmt, 0); } sqlite3_finalize(stmt); return age; } r_status_t db_delete_conversation(db_handle db, const char *session_key) { if (!db || !db->conn || !session_key) return R_ERROR_INVALID_ARG; char *sql = sqlite3_mprintf("DELETE FROM conversations WHERE session_key = %Q", session_key); if (!sql) return R_ERROR_OUT_OF_MEMORY; char *err_msg = NULL; int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg); sqlite3_free(sql); sqlite3_free(err_msg); return (rc == SQLITE_OK) ? R_SUCCESS : R_ERROR_DB_QUERY; } r_status_t db_load_conversation(db_handle db, const char *session_key, char **data) { if (!db || !db->conn || !session_key || !data) return R_ERROR_INVALID_ARG; *data = NULL; const char *sql = "SELECT data FROM conversations WHERE session_key = ?"; sqlite3_stmt *stmt = NULL; if (sqlite3_prepare_v2(db->conn, sql, -1, &stmt, NULL) != SQLITE_OK) { return R_ERROR_DB_QUERY; } sqlite3_bind_text(stmt, 1, session_key, -1, SQLITE_STATIC); int rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char *val = (const char *)sqlite3_column_text(stmt, 0); *data = val ? strdup(val) : NULL; } sqlite3_finalize(stmt); return (rc == SQLITE_ROW) ? R_SUCCESS : R_ERROR_DB_NOT_FOUND; } r_status_t db_snapshot_create(db_handle db, const char *session_id, const char *description, const char **paths, const char **contents, int file_count, long long *snapshot_id_out) { if (!db || !db->conn || !session_id || !paths || !contents || file_count <= 0 || !snapshot_id_out) return R_ERROR_INVALID_ARG; char *err_msg = NULL; if (sqlite3_exec(db->conn, "BEGIN TRANSACTION", NULL, NULL, &err_msg) != SQLITE_OK) { sqlite3_free(err_msg); return R_ERROR_DB_QUERY; } char *sql = sqlite3_mprintf( "INSERT INTO snapshots (session_id, description) VALUES (%Q, %Q)", session_id, description ? description : ""); if (!sql) { sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL); return R_ERROR_OUT_OF_MEMORY; } int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg); sqlite3_free(sql); if (rc != SQLITE_OK) { sqlite3_free(err_msg); sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL); return R_ERROR_DB_QUERY; } long long sid = sqlite3_last_insert_rowid(db->conn); for (int i = 0; i < file_count; i++) { sql = sqlite3_mprintf( "INSERT INTO snapshot_files (snapshot_id, path, content) VALUES (%lld, %Q, %Q)", sid, paths[i], contents[i]); if (!sql) { sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL); return R_ERROR_OUT_OF_MEMORY; } rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg); sqlite3_free(sql); if (rc != SQLITE_OK) { sqlite3_free(err_msg); sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL); return R_ERROR_DB_QUERY; } } if (sqlite3_exec(db->conn, "COMMIT", NULL, NULL, &err_msg) != SQLITE_OK) { sqlite3_free(err_msg); sqlite3_exec(db->conn, "ROLLBACK", NULL, NULL, NULL); return R_ERROR_DB_QUERY; } *snapshot_id_out = sid; return R_SUCCESS; } r_status_t db_snapshot_list(db_handle db, const char *session_id, struct json_object **result) { if (!db || !db->conn || !session_id || !result) return R_ERROR_INVALID_ARG; char *sql = sqlite3_mprintf( "SELECT s.id, s.session_id, s.description, s.created_at, " "(SELECT COUNT(*) FROM snapshot_files sf WHERE sf.snapshot_id = s.id) AS file_count " "FROM snapshots s WHERE s.session_id = %Q ORDER BY s.created_at DESC", session_id); if (!sql) return R_ERROR_OUT_OF_MEMORY; r_status_t status = db_execute(db, sql, result); sqlite3_free(sql); return status; } r_status_t db_snapshot_get_files(db_handle db, long long snapshot_id, struct json_object **result) { if (!db || !db->conn || !result) return R_ERROR_INVALID_ARG; char *sql = sqlite3_mprintf( "SELECT path, content FROM snapshot_files WHERE snapshot_id = %lld", snapshot_id); if (!sql) return R_ERROR_OUT_OF_MEMORY; r_status_t status = db_execute(db, sql, result); sqlite3_free(sql); return status; } r_status_t db_snapshot_ensure_live(db_handle db, const char *session_id, const char *description, long long *snapshot_id_out) { if (!db || !db->conn || !session_id || !snapshot_id_out) return R_ERROR_INVALID_ARG; const char *select_sql = "SELECT id FROM snapshots WHERE session_id = ? LIMIT 1"; sqlite3_stmt *stmt = NULL; if (sqlite3_prepare_v2(db->conn, select_sql, -1, &stmt, NULL) != SQLITE_OK) { return R_ERROR_DB_QUERY; } sqlite3_bind_text(stmt, 1, session_id, -1, SQLITE_STATIC); int rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { *snapshot_id_out = sqlite3_column_int64(stmt, 0); sqlite3_finalize(stmt); char *update_sql = sqlite3_mprintf( "UPDATE snapshots SET description = %Q WHERE id = %lld", description ? description : "", *snapshot_id_out); if (!update_sql) return R_ERROR_OUT_OF_MEMORY; char *err_msg = NULL; rc = sqlite3_exec(db->conn, update_sql, NULL, NULL, &err_msg); sqlite3_free(update_sql); sqlite3_free(err_msg); return (rc == SQLITE_OK) ? R_SUCCESS : R_ERROR_DB_QUERY; } sqlite3_finalize(stmt); char *insert_sql = sqlite3_mprintf( "INSERT INTO snapshots (session_id, description) VALUES (%Q, %Q)", session_id, description ? description : ""); if (!insert_sql) return R_ERROR_OUT_OF_MEMORY; char *err_msg = NULL; rc = sqlite3_exec(db->conn, insert_sql, NULL, NULL, &err_msg); sqlite3_free(insert_sql); if (rc != SQLITE_OK) { sqlite3_free(err_msg); return R_ERROR_DB_QUERY; } *snapshot_id_out = sqlite3_last_insert_rowid(db->conn); return R_SUCCESS; } r_status_t db_snapshot_upsert_file(db_handle db, long long snapshot_id, const char *path, const char *content) { if (!db || !db->conn || !path || !content) return R_ERROR_INVALID_ARG; char *sql = sqlite3_mprintf( "INSERT OR REPLACE INTO snapshot_files (snapshot_id, path, content) " "VALUES (%lld, %Q, %Q)", snapshot_id, path, content); if (!sql) return R_ERROR_OUT_OF_MEMORY; char *err_msg = NULL; int rc = sqlite3_exec(db->conn, sql, NULL, NULL, &err_msg); sqlite3_free(sql); sqlite3_free(err_msg); return (rc == SQLITE_OK) ? R_SUCCESS : R_ERROR_DB_QUERY; }