Compare commits
7 Commits
fix/ai-fol
...
fix/paged-
| Author | SHA1 | Date | |
|---|---|---|---|
|
0096b11d24
|
|||
|
06b80bc89a
|
|||
|
f9e0ba9630
|
|||
|
7469f31c78
|
|||
|
5e329c77e1
|
|||
|
9e5dfb14f8
|
|||
|
1aaa382d5e
|
@@ -1419,10 +1419,10 @@ _ai_request_thread(gpointer data)
|
||||
* list modifications). We must snapshot all fields atomically.
|
||||
* ===================================================================== */
|
||||
pthread_mutex_lock(&session->lock);
|
||||
gchar* local_provider_name = g_strdup(session->provider_name);
|
||||
auto_gchar gchar* local_provider_name = g_strdup(session->provider_name);
|
||||
AIProvider* local_provider = ai_provider_ref(session->provider);
|
||||
gchar* local_model = g_strdup(session->model);
|
||||
gchar* local_api_key = g_strdup(session->api_key ? session->api_key : "");
|
||||
auto_gchar gchar* local_model = g_strdup(session->model);
|
||||
auto_gchar gchar* local_api_key = g_strdup(session->api_key ? session->api_key : "");
|
||||
pthread_mutex_unlock(&session->lock);
|
||||
|
||||
log_debug("[AI-THREAD] Session: %s/%s", local_provider_name, local_model);
|
||||
@@ -1434,6 +1434,8 @@ _ai_request_thread(gpointer data)
|
||||
local_provider_name, local_provider_name);
|
||||
log_error("AI request failed for %s/%s: %s", local_provider_name, local_model, error_msg);
|
||||
_aiwin_display_error(user_data, error_msg);
|
||||
ai_provider_unref(local_provider);
|
||||
ai_session_unref(session);
|
||||
g_free(args);
|
||||
return NULL;
|
||||
}
|
||||
@@ -1444,6 +1446,8 @@ _ai_request_thread(gpointer data)
|
||||
log_error("AI request failed for %s/%s: Failed to initialize curl",
|
||||
local_provider_name, local_model);
|
||||
_aiwin_display_error(user_data, "Failed to initialize curl.");
|
||||
ai_provider_unref(local_provider);
|
||||
ai_session_unref(session);
|
||||
g_free(args);
|
||||
return NULL;
|
||||
}
|
||||
@@ -1492,6 +1496,7 @@ _ai_request_thread(gpointer data)
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
log_debug("[AI-THREAD] HTTP response code: %ld", http_code);
|
||||
auto_gchar gchar* response_data = g_steal_pointer(&response.data);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
auto_gchar gchar* error_msg = g_strdup(curl_easy_strerror(res));
|
||||
@@ -1500,17 +1505,14 @@ _ai_request_thread(gpointer data)
|
||||
} else if (http_code >= 400) {
|
||||
/* Handle HTTP errors */
|
||||
log_debug("[AI-THREAD] HTTP error response body (%zu bytes): %s",
|
||||
response.size, response.data ? response.data : "NULL");
|
||||
response.size, response_data);
|
||||
/* Try to extract the actual error message from the JSON response */
|
||||
auto_gchar gchar* parsed_error = ai_parse_error_message(response.data);
|
||||
auto_gchar gchar* error_msg = parsed_error ? g_strdup_printf("HTTP %ld: %s", http_code, parsed_error) : g_strdup_printf("HTTP %ld: %s", http_code, response.data && strlen(response.data) > 0 ? response.data : "Unknown error");
|
||||
auto_gchar gchar* parsed_error = ai_parse_error_message(response_data);
|
||||
auto_gchar gchar* error_msg = parsed_error ? g_strdup_printf("HTTP %ld: %s", http_code, parsed_error) : g_strdup_printf("HTTP %ld: %s", http_code, response_data && strlen(response_data) > 0 ? response_data : "Unknown error");
|
||||
log_error("AI request failed for %s/%s: %s", local_provider_name, local_model, error_msg);
|
||||
_aiwin_display_error(user_data, error_msg);
|
||||
} else {
|
||||
/* Parse response - transfer ownership to auto_gchar for cleanup */
|
||||
log_debug("[AI-THREAD] Raw API response (%zu bytes): %s", response.size, response.data ? response.data : "NULL");
|
||||
auto_gchar gchar* response_data = response.data;
|
||||
response.data = NULL;
|
||||
log_debug("[AI-THREAD] Raw API response (%zu bytes): %s", response.size, response_data);
|
||||
auto_gchar gchar* content = ai_parse_response(response_data);
|
||||
if (content) {
|
||||
/* Add assistant response to history (under lock) */
|
||||
@@ -1531,12 +1533,9 @@ _ai_request_thread(gpointer data)
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
/* Cleanup local copies */
|
||||
g_free(local_provider_name);
|
||||
ai_provider_unref(local_provider);
|
||||
g_free(local_model);
|
||||
g_free(local_api_key);
|
||||
|
||||
ai_session_unref(session);
|
||||
g_free(args);
|
||||
|
||||
return NULL;
|
||||
|
||||
@@ -8516,7 +8516,8 @@ _cmd_execute_default(ProfWin* window, const char* inp)
|
||||
case WIN_AI:
|
||||
{
|
||||
ProfAiWin* aiwin = (ProfAiWin*)window;
|
||||
cl_ev_send_ai_msg(aiwin, inp, connection_create_stanza_id());
|
||||
auto_gchar gchar* stanza_id = connection_create_stanza_id();
|
||||
cl_ev_send_ai_msg(aiwin, inp, stanza_id);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -10817,7 +10818,6 @@ cmd_ai(ProfWin* window, const char* const command, gchar** args)
|
||||
|
||||
cons_show("Use '/ai start' to begin a chat (uses default provider/model).");
|
||||
cons_show("Use '/ai models <provider>' to fetch available models.");
|
||||
cons_show("Available models: https://models.litellm.ai/");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -10981,6 +10981,9 @@ cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
|
||||
}
|
||||
log_debug("[AI-CMD] wins_new_ai() returned successfully, window type: %d", ai_win->type);
|
||||
|
||||
// Release the reference held by cmd_ai_start, since the window now holds one
|
||||
ai_session_unref(session);
|
||||
|
||||
// Add welcome message to the AI window
|
||||
log_debug("[AI-CMD] Adding welcome messages...");
|
||||
win_println(ai_win, THEME_DEFAULT, "-", "AI Chat: %s/%s", provider_name, model);
|
||||
|
||||
@@ -211,9 +211,13 @@ _first_pass(FILE* fp, const char* basename,
|
||||
|
||||
if (pl->stanza_id && pl->stanza_id[0] != '\0') {
|
||||
if (g_hash_table_contains(seen_stanza_ids, pl->stanza_id)) {
|
||||
*issues = g_slist_prepend(*issues,
|
||||
_issue_new(INTEGRITY_WARNING, basename, lineno,
|
||||
g_strdup_printf("Duplicate stanza-id \"%s\"", pl->stanza_id)));
|
||||
// Older clients (Pidgin, Adium, even early Profanity)
|
||||
// sometimes reuse client-generated stanza-ids across
|
||||
// distinct messages, so a collision is not a real
|
||||
// integrity defect — log it for diagnostics but don't
|
||||
// surface it through /history verify.
|
||||
log_debug("flatfile verify: duplicate stanza-id \"%s\" at %s:%d",
|
||||
pl->stanza_id, basename, lineno);
|
||||
} else {
|
||||
g_hash_table_insert(seen_stanza_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
|
||||
}
|
||||
@@ -302,7 +306,13 @@ _verify_contact_dir(const char* cdir_path, GSList** issues)
|
||||
return;
|
||||
}
|
||||
|
||||
const char* basename = "history.log";
|
||||
// Reconstruct the contact JID from the directory name so issue reports
|
||||
// identify which contact the problem belongs to (every contact has its
|
||||
// own history.log, so the bare filename is ambiguous).
|
||||
auto_gchar gchar* dir_name = g_path_get_basename(cdir_path);
|
||||
auto_gcharv gchar** parts = g_strsplit(dir_name, "_at_", -1);
|
||||
auto_gchar gchar* contact_jid = g_strjoinv("@", parts);
|
||||
auto_gchar gchar* basename = g_strdup_printf("%s/history.log", contact_jid);
|
||||
|
||||
_check_permissions(filepath, basename, issues);
|
||||
|
||||
|
||||
@@ -466,23 +466,36 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// Check timestamp ordering for a specific contact or all
|
||||
// Check timestamp ordering, restricted to consecutive rows belonging
|
||||
// to the same conversation pair so adjacency does not span unrelated
|
||||
// contacts. Adjacent rows by id from different conversations are
|
||||
// legitimately out-of-order and should not be reported.
|
||||
const Jid* myjid = connection_get_jid();
|
||||
if (myjid && myjid->barejid) {
|
||||
auto_sqlite char* query = NULL;
|
||||
if (contact_barejid) {
|
||||
query = sqlite3_mprintf(
|
||||
"SELECT A.`id`, A.`timestamp`, B.`id`, B.`timestamp` FROM `ChatLogs` A "
|
||||
"JOIN `ChatLogs` B ON B.`id` = A.`id` + 1 "
|
||||
"SELECT A.`id`, A.`timestamp`, A.`from_jid`, A.`to_jid`, B.`id`, B.`timestamp` "
|
||||
"FROM `ChatLogs` A "
|
||||
"JOIN `ChatLogs` B ON B.`from_jid` = A.`from_jid` AND B.`to_jid` = A.`to_jid` "
|
||||
" AND B.`id` > A.`id` "
|
||||
"WHERE A.`timestamp` > B.`timestamp` "
|
||||
"AND ((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
|
||||
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` C "
|
||||
" WHERE C.`from_jid` = A.`from_jid` AND C.`to_jid` = A.`to_jid` "
|
||||
" AND C.`id` > A.`id` AND C.`id` < B.`id`) "
|
||||
"LIMIT 50",
|
||||
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid);
|
||||
} else {
|
||||
query = sqlite3_mprintf(
|
||||
"SELECT A.`id`, A.`timestamp`, B.`id`, B.`timestamp` FROM `ChatLogs` A "
|
||||
"JOIN `ChatLogs` B ON B.`id` = A.`id` + 1 "
|
||||
"SELECT A.`id`, A.`timestamp`, A.`from_jid`, A.`to_jid`, B.`id`, B.`timestamp` "
|
||||
"FROM `ChatLogs` A "
|
||||
"JOIN `ChatLogs` B ON B.`from_jid` = A.`from_jid` AND B.`to_jid` = A.`to_jid` "
|
||||
" AND B.`id` > A.`id` "
|
||||
"WHERE A.`timestamp` > B.`timestamp` "
|
||||
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` C "
|
||||
" WHERE C.`from_jid` = A.`from_jid` AND C.`to_jid` = A.`to_jid` "
|
||||
" AND C.`id` > A.`id` AND C.`id` < B.`id`) "
|
||||
"LIMIT 50");
|
||||
}
|
||||
|
||||
@@ -490,15 +503,19 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int id_a = sqlite3_column_int(stmt, 0);
|
||||
const char* ts_a = (const char*)sqlite3_column_text(stmt, 1);
|
||||
int id_b = sqlite3_column_int(stmt, 2);
|
||||
const char* ts_b = (const char*)sqlite3_column_text(stmt, 3);
|
||||
const char* from_a = (const char*)sqlite3_column_text(stmt, 2);
|
||||
const char* to_a = (const char*)sqlite3_column_text(stmt, 3);
|
||||
int id_b = sqlite3_column_int(stmt, 4);
|
||||
const char* ts_b = (const char*)sqlite3_column_text(stmt, 5);
|
||||
|
||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||
issue->level = INTEGRITY_WARNING;
|
||||
issue->file = g_strdup("chatlog.db");
|
||||
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
|
||||
from_a ? from_a : "?", to_a ? to_a : "?");
|
||||
issue->line = id_a;
|
||||
issue->message = g_strdup_printf("Timestamp out of order: row %d (%s) > row %d (%s)",
|
||||
id_a, ts_a ? ts_a : "NULL", id_b, ts_b ? ts_b : "NULL");
|
||||
id_a, ts_a ? ts_a : "NULL",
|
||||
id_b, ts_b ? ts_b : "NULL");
|
||||
issues = g_slist_append(issues, issue);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
@@ -506,7 +523,7 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
||||
|
||||
// Check broken LMC references
|
||||
auto_sqlite char* lmc_query = sqlite3_mprintf(
|
||||
"SELECT A.`id`, A.`replaces_db_id` FROM `ChatLogs` A "
|
||||
"SELECT A.`id`, A.`replaces_db_id`, A.`from_jid`, A.`to_jid` FROM `ChatLogs` A "
|
||||
"WHERE A.`replaces_db_id` IS NOT NULL "
|
||||
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` B WHERE B.`id` = A.`replaces_db_id`) "
|
||||
"LIMIT 50");
|
||||
@@ -515,10 +532,13 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int id = sqlite3_column_int(stmt, 0);
|
||||
int replaces_id = sqlite3_column_int(stmt, 1);
|
||||
const char* from = (const char*)sqlite3_column_text(stmt, 2);
|
||||
const char* to = (const char*)sqlite3_column_text(stmt, 3);
|
||||
|
||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||
issue->level = INTEGRITY_ERROR;
|
||||
issue->file = g_strdup("chatlog.db");
|
||||
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
|
||||
from ? from : "?", to ? to : "?");
|
||||
issue->line = id;
|
||||
issue->message = g_strdup_printf("Broken LMC reference: row %d references non-existent row %d",
|
||||
id, replaces_id);
|
||||
|
||||
@@ -848,6 +848,14 @@ win_page_down(ProfWin* window, int scroll_size)
|
||||
window->layout->paged = 0;
|
||||
window->layout->unread_msg = 0;
|
||||
}
|
||||
|
||||
// Non-chat windows have no DB to fetch from, so WIN_SCROLL_REACHED_BOTTOM
|
||||
// never fires; reset paged once the buffer's last line is on screen.
|
||||
if (window->type != WIN_CHAT
|
||||
&& (total_rows - *page_start) <= page_space + 1) {
|
||||
window->layout->paged = 0;
|
||||
window->layout->unread_msg = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "glib.h"
|
||||
#include "prof_cmocka.h"
|
||||
#include "common.h"
|
||||
#include "ai/ai_client.h"
|
||||
@@ -703,7 +704,6 @@ test_ai_parse_models_openai_format(void** state)
|
||||
models = g_list_next(models);
|
||||
assert_string_equal("google/gemini-3-flash-preview", models->data);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -776,7 +776,6 @@ test_ai_parse_models_perplexity_format(void** state)
|
||||
models = g_list_last(models);
|
||||
assert_string_equal("xai/grok-4.3", models->data);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -801,7 +800,6 @@ test_ai_parse_models_array_format(void** state)
|
||||
models = g_list_next(models);
|
||||
assert_string_equal("model3", models->data);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -821,7 +819,6 @@ test_ai_parse_models_empty_json(void** state)
|
||||
assert_null(provider->models);
|
||||
assert_int_equal(0, g_list_length(provider->models));
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -837,7 +834,6 @@ test_ai_parse_models_null_handling(void** state)
|
||||
ai_parse_models_from_json(provider, NULL);
|
||||
assert_null(provider->models);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -861,7 +857,6 @@ test_ai_parse_models_escaped_quotes(void** state)
|
||||
GList* models = provider->models;
|
||||
assert_non_null(models);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -885,7 +880,6 @@ test_ai_parse_models_with_whitespace(void** state)
|
||||
GList* models = provider->models;
|
||||
assert_string_equal("model-with-spaces", models->data);
|
||||
|
||||
ai_provider_unref(provider);
|
||||
ai_client_shutdown();
|
||||
}
|
||||
|
||||
@@ -1142,7 +1136,7 @@ test_ai_providers_find_cycles_through_many(void** state)
|
||||
ai_add_provider("alpine", "https://al.example/");
|
||||
|
||||
/* Three providers share prefix "alp": alpha, alphabet, alpine. */
|
||||
GHashTable* seen = g_hash_table_new(g_str_hash, g_str_equal);
|
||||
GHashTable* seen = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
auto_gchar gchar* match = ai_providers_find("alp", FALSE, NULL);
|
||||
|
||||
Reference in New Issue
Block a user