From 3365db45b9e4a405d2ac67af81d1768bcb82497d Mon Sep 17 00:00:00 2001 From: crispyberry Date: Thu, 5 Feb 2026 18:55:44 +0800 Subject: [PATCH] feat: add memory store and session manager MEMORY.md for long-term memory, daily YYYY-MM-DD.md notes. JSONL session files per chat_id with ring buffer history (max 20 messages). All persisted on SPIFFS. Co-Authored-By: Claude Opus 4.5 --- main/memory/memory_store.c | 107 ++++++++++++++++++++++++ main/memory/memory_store.h | 31 +++++++ main/memory/session_mgr.c | 165 +++++++++++++++++++++++++++++++++++++ main/memory/session_mgr.h | 39 +++++++++ 4 files changed, 342 insertions(+) create mode 100644 main/memory/memory_store.c create mode 100644 main/memory/memory_store.h create mode 100644 main/memory/session_mgr.c create mode 100644 main/memory/session_mgr.h diff --git a/main/memory/memory_store.c b/main/memory/memory_store.c new file mode 100644 index 0000000..33f72e0 --- /dev/null +++ b/main/memory/memory_store.c @@ -0,0 +1,107 @@ +#include "memory_store.h" +#include "mimi_config.h" + +#include +#include +#include +#include +#include "esp_log.h" + +static const char *TAG = "memory"; + +static void get_date_str(char *buf, size_t size, int days_ago) +{ + time_t now; + time(&now); + now -= days_ago * 86400; + struct tm tm; + localtime_r(&now, &tm); + strftime(buf, size, "%Y-%m-%d", &tm); +} + +esp_err_t memory_store_init(void) +{ + /* SPIFFS is flat — no real directory creation needed. + Just verify we can open the base path. */ + ESP_LOGI(TAG, "Memory store initialized at %s", MIMI_SPIFFS_BASE); + return ESP_OK; +} + +esp_err_t memory_read_long_term(char *buf, size_t size) +{ + FILE *f = fopen(MIMI_MEMORY_FILE, "r"); + if (!f) { + buf[0] = '\0'; + return ESP_ERR_NOT_FOUND; + } + + size_t n = fread(buf, 1, size - 1, f); + buf[n] = '\0'; + fclose(f); + return ESP_OK; +} + +esp_err_t memory_write_long_term(const char *content) +{ + FILE *f = fopen(MIMI_MEMORY_FILE, "w"); + if (!f) { + ESP_LOGE(TAG, "Cannot write %s", MIMI_MEMORY_FILE); + return ESP_FAIL; + } + fputs(content, f); + fclose(f); + ESP_LOGI(TAG, "Long-term memory updated (%d bytes)", (int)strlen(content)); + return ESP_OK; +} + +esp_err_t memory_append_today(const char *note) +{ + char date_str[16]; + get_date_str(date_str, sizeof(date_str), 0); + + char path[64]; + snprintf(path, sizeof(path), "%s/%s.md", MIMI_SPIFFS_MEMORY_DIR, date_str); + + FILE *f = fopen(path, "a"); + if (!f) { + /* Try creating — if file doesn't exist yet, write header */ + f = fopen(path, "w"); + if (!f) { + ESP_LOGE(TAG, "Cannot open %s", path); + return ESP_FAIL; + } + fprintf(f, "# %s\n\n", date_str); + } + + fprintf(f, "%s\n", note); + fclose(f); + return ESP_OK; +} + +esp_err_t memory_read_recent(char *buf, size_t size, int days) +{ + size_t offset = 0; + buf[0] = '\0'; + + for (int i = 0; i < days && offset < size - 1; i++) { + char date_str[16]; + get_date_str(date_str, sizeof(date_str), i); + + char path[64]; + snprintf(path, sizeof(path), "%s/%s.md", MIMI_SPIFFS_MEMORY_DIR, date_str); + + FILE *f = fopen(path, "r"); + if (!f) continue; + + if (offset > 0 && offset < size - 4) { + offset += snprintf(buf + offset, size - offset, "\n---\n"); + } + + size_t n = fread(buf + offset, 1, size - offset - 1, f); + offset += n; + buf[offset] = '\0'; + fclose(f); + } + + return ESP_OK; +} diff --git a/main/memory/memory_store.h b/main/memory/memory_store.h new file mode 100644 index 0000000..9e14abb --- /dev/null +++ b/main/memory/memory_store.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esp_err.h" +#include + +/** + * Initialize memory store. Ensures SPIFFS directories exist. + */ +esp_err_t memory_store_init(void); + +/** + * Read long-term memory (MEMORY.md) into buffer. + * @return ESP_OK on success, ESP_ERR_NOT_FOUND if file missing + */ +esp_err_t memory_read_long_term(char *buf, size_t size); + +/** + * Write content to long-term memory (MEMORY.md). + */ +esp_err_t memory_write_long_term(const char *content); + +/** + * Append a note to today's daily memory file (YYYY-MM-DD.md). + */ +esp_err_t memory_append_today(const char *note); + +/** + * Read recent daily memories (last N days) into buffer. + * @param days Number of days to look back (default 3) + */ +esp_err_t memory_read_recent(char *buf, size_t size, int days); diff --git a/main/memory/session_mgr.c b/main/memory/session_mgr.c new file mode 100644 index 0000000..5579934 --- /dev/null +++ b/main/memory/session_mgr.c @@ -0,0 +1,165 @@ +#include "session_mgr.h" +#include "mimi_config.h" + +#include +#include +#include +#include +#include +#include "esp_log.h" +#include "cJSON.h" + +static const char *TAG = "session"; + +static void session_path(const char *chat_id, char *buf, size_t size) +{ + snprintf(buf, size, "%s/tg_%s.jsonl", MIMI_SPIFFS_SESSION_DIR, chat_id); +} + +esp_err_t session_mgr_init(void) +{ + ESP_LOGI(TAG, "Session manager initialized at %s", MIMI_SPIFFS_SESSION_DIR); + return ESP_OK; +} + +esp_err_t session_append(const char *chat_id, const char *role, const char *content) +{ + char path[64]; + session_path(chat_id, path, sizeof(path)); + + FILE *f = fopen(path, "a"); + if (!f) { + ESP_LOGE(TAG, "Cannot open session file %s", path); + return ESP_FAIL; + } + + cJSON *obj = cJSON_CreateObject(); + cJSON_AddStringToObject(obj, "role", role); + cJSON_AddStringToObject(obj, "content", content); + cJSON_AddNumberToObject(obj, "ts", (double)time(NULL)); + + char *line = cJSON_PrintUnformatted(obj); + cJSON_Delete(obj); + + if (line) { + fprintf(f, "%s\n", line); + free(line); + } + + fclose(f); + return ESP_OK; +} + +esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size, int max_msgs) +{ + char path[64]; + session_path(chat_id, path, sizeof(path)); + + FILE *f = fopen(path, "r"); + if (!f) { + /* No history yet */ + snprintf(buf, size, "[]"); + return ESP_OK; + } + + /* Read all lines into a ring buffer of cJSON objects */ + cJSON *messages[MIMI_SESSION_MAX_MSGS]; + int count = 0; + int write_idx = 0; + + char line[2048]; + while (fgets(line, sizeof(line), f)) { + /* Strip newline */ + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0'; + if (line[0] == '\0') continue; + + cJSON *obj = cJSON_Parse(line); + if (!obj) continue; + + /* Ring buffer: overwrite oldest if full */ + if (count >= max_msgs) { + cJSON_Delete(messages[write_idx]); + } + messages[write_idx] = obj; + write_idx = (write_idx + 1) % max_msgs; + if (count < max_msgs) count++; + } + fclose(f); + + /* Build JSON array with only role + content */ + cJSON *arr = cJSON_CreateArray(); + int start = (count < max_msgs) ? 0 : write_idx; + for (int i = 0; i < count; i++) { + int idx = (start + i) % max_msgs; + cJSON *src = messages[idx]; + + cJSON *entry = cJSON_CreateObject(); + cJSON *role = cJSON_GetObjectItem(src, "role"); + cJSON *content = cJSON_GetObjectItem(src, "content"); + if (role && content) { + cJSON_AddStringToObject(entry, "role", role->valuestring); + cJSON_AddStringToObject(entry, "content", content->valuestring); + } + cJSON_AddItemToArray(arr, entry); + } + + /* Cleanup ring buffer */ + int cleanup_start = (count < max_msgs) ? 0 : write_idx; + for (int i = 0; i < count; i++) { + int idx = (cleanup_start + i) % max_msgs; + cJSON_Delete(messages[idx]); + } + + char *json_str = cJSON_PrintUnformatted(arr); + cJSON_Delete(arr); + + if (json_str) { + strncpy(buf, json_str, size - 1); + buf[size - 1] = '\0'; + free(json_str); + } else { + snprintf(buf, size, "[]"); + } + + return ESP_OK; +} + +esp_err_t session_clear(const char *chat_id) +{ + char path[64]; + session_path(chat_id, path, sizeof(path)); + + if (remove(path) == 0) { + ESP_LOGI(TAG, "Session %s cleared", chat_id); + return ESP_OK; + } + return ESP_ERR_NOT_FOUND; +} + +void session_list(void) +{ + DIR *dir = opendir(MIMI_SPIFFS_SESSION_DIR); + if (!dir) { + /* SPIFFS is flat, so list all files matching pattern */ + dir = opendir(MIMI_SPIFFS_BASE); + if (!dir) { + ESP_LOGW(TAG, "Cannot open SPIFFS directory"); + return; + } + } + + struct dirent *entry; + int count = 0; + while ((entry = readdir(dir)) != NULL) { + if (strstr(entry->d_name, "tg_") && strstr(entry->d_name, ".jsonl")) { + ESP_LOGI(TAG, " Session: %s", entry->d_name); + count++; + } + } + closedir(dir); + + if (count == 0) { + ESP_LOGI(TAG, " No sessions found"); + } +} diff --git a/main/memory/session_mgr.h b/main/memory/session_mgr.h new file mode 100644 index 0000000..73fe870 --- /dev/null +++ b/main/memory/session_mgr.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esp_err.h" +#include + +/** + * Initialize session manager. + */ +esp_err_t session_mgr_init(void); + +/** + * Append a message to a session file (JSONL format). + * @param chat_id Session identifier (e.g., "12345") + * @param role "user" or "assistant" + * @param content Message text + */ +esp_err_t session_append(const char *chat_id, const char *role, const char *content); + +/** + * Load session history as a JSON array string suitable for LLM messages. + * Returns the last max_msgs messages as: + * [{"role":"user","content":"..."},{"role":"assistant","content":"..."},...] + * + * @param chat_id Session identifier + * @param buf Output buffer (caller allocates) + * @param size Buffer size + * @param max_msgs Maximum number of messages to return + */ +esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size, int max_msgs); + +/** + * Clear a session (delete the file). + */ +esp_err_t session_clear(const char *chat_id); + +/** + * List all session files (prints to log). + */ +void session_list(void);