#include "storage/store.h" #include "storage/codec.h" #include "sys/system.h" #include #include #include #include "base/allocator.h" #define RECORD_HEADER_SIZE 24U #define RECORD_VERSION 2U #define RECORD_VERSION_LEGACY 1U #define RECORD_PUT 1U #define RECORD_DEL 2U #define RECORD_MARKER 3U #define RECORD_UPDATE 4U #define UPDATE_CONTENT 1U #define UPDATE_METADATA 2U #define RECORD_PAYLOAD_MAX (TINY_CONTENT_MAX + 16384U) #define SNAPSHOT_HEADER_SIZE 52U struct tiny_store { tiny_sys_file_t lock; tiny_sys_file_t log; char *directory; char *log_path; char *snapshot_path; tiny_store_entry_t *entries; size_t entry_count; size_t entry_capacity; uint64_t last_sequence; uint64_t update_records; uint64_t dead_bytes; uint64_t last_ulid_ms; unsigned char last_random[10]; unsigned char generation[16]; bool has_generation; bool recovered_tail; bool snapshot_valid; char last_error[160]; }; static void set_error(tiny_store_t *store, const char *message) { snprintf(store->last_error, sizeof(store->last_error), "%s", message); } static bool write_all(tiny_sys_file_t *file, const void *data, uint32_t length) { const unsigned char *position = data; while (length > 0) { uint32_t written = 0; if (!tiny_sys_file_write(file, position, length, &written) || written == 0) { return false; } position += written; length -= written; } return true; } static bool read_all(tiny_sys_file_t *file, void *data, uint32_t length, uint32_t *actual) { unsigned char *position = data; uint32_t total = 0; while (total < length) { uint32_t count = 0; if (!tiny_sys_file_read(file, position + total, length - total, &count)) { return false; } if (count == 0) { break; } total += count; } *actual = total; return true; } static uint64_t now_milliseconds(void) { return tiny_sys_now_milliseconds(); } static void make_ulid(tiny_store_t *store, uint64_t timestamp, char output[27]) { static const char alphabet[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; unsigned char bytes[16]; unsigned index; if (timestamp > store->last_ulid_ms) { store->last_ulid_ms = timestamp; if (!tiny_sys_random_bytes(store->last_random, sizeof(store->last_random))) { uint64_t fallback = tiny_sys_monotonic_milliseconds() ^ timestamp; for (index = 0; index < sizeof(store->last_random); ++index) { store->last_random[index] = (unsigned char)(fallback >> ((index % 8) * 8)); } } } else { timestamp = store->last_ulid_ms; for (index = 10; index-- > 0;) { if (++store->last_random[index] != 0) { break; } } } bytes[0] = (unsigned char)(timestamp >> 40); bytes[1] = (unsigned char)(timestamp >> 32); bytes[2] = (unsigned char)(timestamp >> 24); bytes[3] = (unsigned char)(timestamp >> 16); bytes[4] = (unsigned char)(timestamp >> 8); bytes[5] = (unsigned char)timestamp; memcpy(bytes + 6, store->last_random, 10); for (index = 0; index < 26; ++index) { const unsigned bit = index * 5; unsigned value = 0, part; for (part = 0; part < 5; ++part) { const int source_bit = (int)bit + (int)part - 2; value <<= 1; if (source_bit >= 0) { value |= (bytes[source_bit / 8] >> (7 - source_bit % 8)) & 1U; } } output[index] = alphabet[value]; } output[26] = '\0'; } static bool ensure_entries(tiny_store_t *store) { tiny_store_entry_t *resized; size_t capacity; if (store->entry_count < store->entry_capacity) { return true; } capacity = store->entry_capacity == 0 ? 32 : store->entry_capacity * 2; resized = realloc(store->entries, capacity * sizeof(*resized)); if (resized == NULL) { return false; } store->entries = resized; store->entry_capacity = capacity; return true; } static tiny_store_entry_t * find_entry(tiny_store_t *store, const char *message_id) { size_t index; for (index = 0; index < store->entry_count; ++index) { if (strcmp(store->entries[index].message.message_id, message_id) == 0) { return &store->entries[index]; } } return NULL; } static bool serialize_put(const tiny_message_t *message, unsigned char **payload, uint32_t *length) { size_t session_len = strlen(message->session_id), content_len = strlen(message->content); size_t tool_len = message->metadata.tool_call_id ? strlen(message->metadata.tool_call_id) : 0; size_t summary_len = message->metadata.summary ? strlen(message->metadata.summary) : 0; size_t total = 8 + 26 + 2 + 16 + session_len + content_len + tool_len; size_t index, offset; unsigned char *data; for (index = 0; index < message->metadata.tag_count; ++index) { total += 4 + strlen(message->metadata.tags[index]); } total += 32 + summary_len + message->metadata.relation_count * 27 + message->metadata.source_count * 26; if (total > UINT32_MAX || total > RECORD_PAYLOAD_MAX) { return false; } data = malloc(total); if (data == NULL) { return false; } offset = 0; tiny_codec_put_u64(data + offset, (uint64_t)message->timestamp_ms); offset += 8; memcpy(data + offset, message->message_id, 26); offset += 26; data[offset++] = (unsigned char)message->role; data[offset++] = (unsigned char)message->metadata.memory_tier; tiny_codec_put_u32(data + offset, (uint32_t)session_len); offset += 4; tiny_codec_put_u32(data + offset, (uint32_t)content_len); offset += 4; tiny_codec_put_u32(data + offset, (uint32_t)tool_len); offset += 4; tiny_codec_put_u32(data + offset, (uint32_t)message->metadata.tag_count); offset += 4; memcpy(data + offset, message->session_id, session_len); offset += session_len; memcpy(data + offset, message->content, content_len); offset += content_len; if (tool_len > 0) { memcpy(data + offset, message->metadata.tool_call_id, tool_len); offset += tool_len; } for (index = 0; index < message->metadata.tag_count; ++index) { const size_t tag_len = strlen(message->metadata.tags[index]); tiny_codec_put_u32(data + offset, (uint32_t)tag_len); offset += 4; memcpy(data + offset, message->metadata.tags[index], tag_len); offset += tag_len; } memcpy(data + offset, "M2", 2); offset += 2; tiny_codec_put_u64(data + offset, message->revision); offset += 8; tiny_codec_put_u64(data + offset, (uint64_t)message->updated_at_ms); offset += 8; data[offset++] = (unsigned char)message->metadata.memory_kind; data[offset++] = message->metadata.importance; tiny_codec_put_u32(data + offset, (uint32_t)summary_len); offset += 4; tiny_codec_put_u32(data + offset, (uint32_t)message->metadata.relation_count); offset += 4; tiny_codec_put_u32(data + offset, (uint32_t)message->metadata.source_count); offset += 4; if (summary_len > 0) { memcpy(data + offset, message->metadata.summary, summary_len); offset += summary_len; } for (index = 0; index < message->metadata.relation_count; ++index) { data[offset++] = (unsigned char)message->metadata.relations[index].type; memcpy(data + offset, message->metadata.relations[index].message_id, 26); offset += 26; } for (index = 0; index < message->metadata.source_count; ++index) { memcpy(data + offset, message->metadata.source_message_ids[index], 26); offset += 26; } *payload = data; *length = (uint32_t)total; return true; } static char * copy_field(const unsigned char *data, size_t length) { char *value = malloc(length + 1); if (value != NULL) { memcpy(value, data, length); value[length] = '\0'; } return value; } static bool deserialize_failed(tiny_message_t *message) { tiny_message_free(message); return false; } static bool deserialize_put(const unsigned char *data, uint32_t length, tiny_message_t *message) { uint32_t session_len, content_len, tool_len, tag_count; size_t offset = 0, index; tiny_message_init(message); if (length < 8 + 26 + 2 + 16) { return false; } message->timestamp_ms = (int64_t)tiny_codec_get_u64(data + offset); offset += 8; memcpy(message->message_id, data + offset, 26); message->message_id[26] = '\0'; offset += 26; message->role = (tiny_role_t)data[offset++]; message->metadata.memory_tier = (tiny_memory_tier_t)data[offset++]; session_len = tiny_codec_get_u32(data + offset); offset += 4; content_len = tiny_codec_get_u32(data + offset); offset += 4; tool_len = tiny_codec_get_u32(data + offset); offset += 4; tag_count = tiny_codec_get_u32(data + offset); offset += 4; if (!tiny_message_id_valid(message->message_id) || (message->role < TINY_ROLE_USER || message->role > TINY_ROLE_SYSTEM) || (message->metadata.memory_tier < TINY_TIER_SHORT || message->metadata.memory_tier > TINY_TIER_DISCARD) || session_len == 0 || session_len > TINY_SESSION_ID_MAX || content_len == 0 || content_len > TINY_CONTENT_MAX || tool_len > TINY_TOOL_CALL_ID_MAX || tag_count > TINY_TAG_COUNT_MAX || offset + session_len + content_len + tool_len > length) { return false; } message->session_id = copy_field(data + offset, session_len); offset += session_len; message->content = copy_field(data + offset, content_len); offset += content_len; if (tool_len > 0) { message->metadata.tool_call_id = copy_field(data + offset, tool_len); offset += tool_len; } if (message->session_id == NULL || message->content == NULL || (tool_len && !message->metadata.tool_call_id)) { return deserialize_failed(message); } if (tag_count > 0) { message->metadata.tags = calloc(tag_count, sizeof(char *)); if (message->metadata.tags == NULL) { return deserialize_failed(message); } for (index = 0; index < tag_count; ++index) { uint32_t tag_len; if (offset + 4 > length) { return deserialize_failed(message); } tag_len = tiny_codec_get_u32(data + offset); offset += 4; if (tag_len == 0 || tag_len > TINY_TAG_MAX || offset + tag_len > length) { return deserialize_failed(message); } message->metadata.tags[index] = copy_field(data + offset, tag_len); offset += tag_len; if (message->metadata.tags[index] == NULL) { return deserialize_failed(message); } message->metadata.tag_count++; } } if (!tiny_utf8_valid(message->session_id, session_len) || !tiny_utf8_valid(message->content, content_len)) { return deserialize_failed(message); } if (offset == length) { message->updated_at_ms = message->timestamp_ms; return true; } { uint32_t summary_len, relation_count, source_count; if (length - offset < 32 || memcmp(data + offset, "M2", 2) != 0) { return deserialize_failed(message); } offset += 2; message->revision = tiny_codec_get_u64(data + offset); offset += 8; message->updated_at_ms = (int64_t)tiny_codec_get_u64(data + offset); offset += 8; message->metadata.memory_kind = (tiny_memory_kind_t)data[offset++]; message->metadata.importance = data[offset++]; summary_len = tiny_codec_get_u32(data + offset); offset += 4; relation_count = tiny_codec_get_u32(data + offset); offset += 4; source_count = tiny_codec_get_u32(data + offset); offset += 4; if (message->revision == 0 || message->metadata.memory_kind < TINY_KIND_CONVERSATION || message->metadata.memory_kind > TINY_KIND_SUMMARY || summary_len > TINY_SUMMARY_MAX || relation_count > TINY_RELATION_COUNT_MAX || source_count > TINY_SOURCE_COUNT_MAX || offset + summary_len + (size_t)relation_count * 27 + (size_t)source_count * 26 != length) { return deserialize_failed(message); } if (summary_len > 0) { message->metadata.summary = copy_field(data + offset, summary_len); if (!message->metadata.summary || !tiny_utf8_valid(message->metadata.summary, summary_len)) { return deserialize_failed(message); } offset += summary_len; } if (relation_count > 0) { message->metadata.relations = calloc(relation_count, sizeof(tiny_relation_t)); if (!message->metadata.relations) { return deserialize_failed(message); } for (index = 0; index < relation_count; ++index) { tiny_relation_t *relation = &message->metadata.relations[index]; relation->type = (tiny_relation_type_t)data[offset++]; memcpy(relation->message_id, data + offset, 26); relation->message_id[26] = '\0'; offset += 26; if (relation->type < TINY_RELATION_DEPENDS_ON || relation->type > TINY_RELATION_RELATED_TO || !tiny_message_id_valid(relation->message_id)) { return deserialize_failed(message); } message->metadata.relation_count++; } } if (source_count > 0) { message->metadata.source_message_ids = calloc(source_count, sizeof(char *)); if (!message->metadata.source_message_ids) { return deserialize_failed(message); } for (index = 0; index < source_count; ++index) { message->metadata.source_message_ids[index] = copy_field(data + offset, 26); offset += 26; if (!message->metadata.source_message_ids[index] || !tiny_message_id_valid(message->metadata.source_message_ids[index])) { return deserialize_failed(message); } message->metadata.source_count++; } } } return offset == length; } static tiny_store_status_t append_record_to(tiny_sys_file_t *file, uint8_t type, uint64_t sequence, const unsigned char *payload, uint32_t payload_length, bool flush) { unsigned char header[RECORD_HEADER_SIZE] = { 0 }; memcpy(header, "TCMR", 4); header[4] = RECORD_VERSION; header[5] = type; tiny_codec_put_u32(header + 8, payload_length); tiny_codec_put_u64(header + 12, sequence); tiny_codec_put_u32(header + 20, tiny_codec_crc32(payload, payload_length)); if (!tiny_sys_file_seek(file, 0, TINY_SYS_SEEK_END) || !write_all(file, header, sizeof(header)) || !write_all(file, payload, payload_length) || (flush && !tiny_sys_file_flush(file))) { return TINY_STORE_IO_ERROR; } return TINY_STORE_OK; } static tiny_store_status_t apply_put(tiny_store_t *store, tiny_message_t *message, uint64_t sequence) { tiny_store_entry_t *entry = find_entry(store, message->message_id); if (entry != NULL) { return TINY_STORE_CORRUPT; } if (!ensure_entries(store)) { return TINY_STORE_NO_MEMORY; } entry = &store->entries[store->entry_count++]; memset(entry, 0, sizeof(*entry)); entry->message = *message; tiny_message_init(message); entry->sequence = sequence; return TINY_STORE_OK; } static bool string_equal(const char *left, const char *right) { return left == right || (left && right && strcmp(left, right) == 0); } static bool metadata_patch_changes(const tiny_metadata_t *current, const tiny_metadata_t *patch, tiny_metadata_mask_t mask) { tiny_metadata_t merged = *current; if (mask & TINY_METADATA_TOOL_CALL_ID) { merged.tool_call_id = patch->tool_call_id; } if (mask & TINY_METADATA_TAGS) { merged.tags = patch->tags; merged.tag_count = patch->tag_count; } if (mask & TINY_METADATA_TIER) { merged.memory_tier = patch->memory_tier; } if (mask & TINY_METADATA_KIND) { merged.memory_kind = patch->memory_kind; } if (mask & TINY_METADATA_IMPORTANCE) { merged.importance = patch->importance; } if (mask & TINY_METADATA_SUMMARY) { merged.summary = patch->summary; } if (mask & TINY_METADATA_RELATIONS) { merged.relations = patch->relations; merged.relation_count = patch->relation_count; } if (mask & TINY_METADATA_SOURCES) { merged.source_message_ids = patch->source_message_ids; merged.source_count = patch->source_count; } if (!string_equal(current->tool_call_id, merged.tool_call_id) || current->tag_count != merged.tag_count || current->memory_tier != merged.memory_tier || current->memory_kind != merged.memory_kind || current->importance != merged.importance || !string_equal(current->summary, merged.summary) || current->relation_count != merged.relation_count || current->source_count != merged.source_count) { return true; } if (current->tag_count) { size_t index; for (index = 0; index < current->tag_count; ++index) { if (strcmp(current->tags[index], merged.tags[index]) != 0) { return true; } } } if (current->relation_count && memcmp(current->relations, merged.relations, current->relation_count * sizeof(*current->relations)) != 0) { return true; } if (current->source_count) { size_t index; for (index = 0; index < current->source_count; ++index) { if (strcmp(current->source_message_ids[index], merged.source_message_ids[index]) != 0) { return true; } } } return false; } static bool metadata_targets_valid(tiny_store_t *store, const char *self_id, const tiny_metadata_t *metadata) { size_t index, scan; if (!tiny_metadata_valid(metadata)) { return false; } for (index = 0; index < metadata->relation_count; ++index) { tiny_store_entry_t *target; const char *id = metadata->relations[index].message_id; if (self_id && strcmp(self_id, id) == 0) { return false; } target = find_entry(store, id); if (!target || target->deleted) { return false; } for (scan = 0; scan < index; ++scan) { if (strcmp(metadata->relations[scan].message_id, id) == 0) { return false; } } } for (index = 0; index < metadata->source_count; ++index) { tiny_store_entry_t *target; const char *id = metadata->source_message_ids[index]; if (self_id && strcmp(self_id, id) == 0) { return false; } target = find_entry(store, id); if (!target || target->deleted) { return false; } for (scan = 0; scan < index; ++scan) { if (strcmp(metadata->source_message_ids[scan], id) == 0) { return false; } } } return true; } static bool prune_metadata_references(tiny_store_t *store, tiny_message_t *message) { size_t index, write_index; bool changed = false; if (!store || !message) { return false; } /* Prune relations whose targets are deleted or missing. */ for (index = 0, write_index = 0; index < message->metadata.relation_count; ++index) { tiny_store_entry_t *target = find_entry(store, message->metadata.relations[index].message_id); if (target && !target->deleted) { if (write_index != index) { message->metadata.relations[write_index] = message->metadata.relations[index]; } write_index++; } else { changed = true; } } if (changed) { message->metadata.relation_count = write_index; if (write_index == 0) { free(message->metadata.relations); message->metadata.relations = NULL; } } /* Prune sources whose targets are deleted or missing. */ changed = false; for (index = 0, write_index = 0; index < message->metadata.source_count; ++index) { tiny_store_entry_t *target = find_entry(store, message->metadata.source_message_ids[index]); if (target && !target->deleted) { if (write_index != index) { message->metadata.source_message_ids[write_index] = message->metadata.source_message_ids[index]; } write_index++; } else { changed = true; } } if (changed) { message->metadata.source_count = write_index; if (write_index == 0) { free(message->metadata.source_message_ids); message->metadata.source_message_ids = NULL; } } return true; } static tiny_store_status_t prepare_update(tiny_store_t *store, tiny_store_entry_t *entry, const tiny_message_patch_t *patch, uint64_t revision, int64_t updated_at, tiny_message_t *prepared) { tiny_message_t merged = entry->message; tiny_metadata_t metadata = entry->message.metadata; if (patch->content) { merged.content = (char *)patch->content; } if (patch->metadata_mask & TINY_METADATA_TOOL_CALL_ID) { metadata.tool_call_id = patch->metadata->tool_call_id; } if (patch->metadata_mask & TINY_METADATA_TAGS) { metadata.tags = patch->metadata->tags; metadata.tag_count = patch->metadata->tag_count; } if (patch->metadata_mask & TINY_METADATA_TIER) { metadata.memory_tier = patch->metadata->memory_tier; } if (patch->metadata_mask & TINY_METADATA_KIND) { metadata.memory_kind = patch->metadata->memory_kind; } if (patch->metadata_mask & TINY_METADATA_IMPORTANCE) { metadata.importance = patch->metadata->importance; } if (patch->metadata_mask & TINY_METADATA_SUMMARY) { metadata.summary = patch->metadata->summary; } if (patch->metadata_mask & TINY_METADATA_RELATIONS) { metadata.relations = patch->metadata->relations; metadata.relation_count = patch->metadata->relation_count; } if (patch->metadata_mask & TINY_METADATA_SOURCES) { metadata.source_message_ids = patch->metadata->source_message_ids; metadata.source_count = patch->metadata->source_count; } merged.metadata = metadata; if (!merged.content || !merged.content[0] || strlen(merged.content) > TINY_CONTENT_MAX || !tiny_utf8_valid(merged.content, strlen(merged.content)) || !metadata_targets_valid(store, merged.message_id, &metadata)) { return TINY_STORE_CONFLICT; } if (tiny_message_copy(prepared, &merged) != 0) { return TINY_STORE_NO_MEMORY; } prepared->revision = revision; prepared->updated_at_ms = updated_at; return TINY_STORE_OK; } static bool delta_put_string(unsigned char *data, size_t capacity, size_t *offset, const char *value) { size_t length = value ? strlen(value) : 0; if (length > UINT32_MAX || *offset > capacity || capacity - *offset < 4 + length) { return false; } tiny_codec_put_u32(data + *offset, (uint32_t)length); *offset += 4; if (length) { memcpy(data + *offset, value, length); *offset += length; } return true; } static bool serialize_update(const tiny_store_entry_t *entry, const tiny_message_patch_t *patch, uint64_t revision, int64_t updated_at, unsigned char **payload, uint32_t *payload_length) { size_t capacity = 64 + (patch->content ? strlen(patch->content) : 0), offset = 0, index; unsigned char *data; tiny_metadata_mask_t mask = patch->metadata_mask; if (mask & TINY_METADATA_TOOL_CALL_ID) { capacity += 4 + (patch->metadata->tool_call_id ? strlen(patch->metadata->tool_call_id) : 0); } if (mask & TINY_METADATA_TAGS) { capacity += 4; for (index = 0; index < patch->metadata->tag_count; ++index) { capacity += 4 + strlen(patch->metadata->tags[index]); } } if (mask & TINY_METADATA_SUMMARY) { capacity += 4 + (patch->metadata->summary ? strlen(patch->metadata->summary) : 0); } if (mask & TINY_METADATA_RELATIONS) { capacity += 4 + patch->metadata->relation_count * 27; } if (mask & TINY_METADATA_SOURCES) { capacity += 4 + patch->metadata->source_count * 26; } capacity += (mask & TINY_METADATA_TIER) ? 1 : 0; capacity += (mask & TINY_METADATA_KIND) ? 1 : 0; capacity += (mask & TINY_METADATA_IMPORTANCE) ? 1 : 0; if (capacity > RECORD_PAYLOAD_MAX || capacity > UINT32_MAX || !(data = malloc(capacity))) { return false; } memcpy(data, entry->message.message_id, 26); offset = 26; tiny_codec_put_u64(data + offset, revision); offset += 8; tiny_codec_put_u64(data + offset, (uint64_t)updated_at); offset += 8; data[offset++] = patch->content ? UPDATE_CONTENT : 0; data[offset - 1] |= mask ? UPDATE_METADATA : 0; tiny_codec_put_u32(data + offset, mask); offset += 4; if (patch->content && !delta_put_string(data, capacity, &offset, patch->content)) { free(data); return false; } if ((mask & TINY_METADATA_TOOL_CALL_ID) && !delta_put_string(data, capacity, &offset, patch->metadata->tool_call_id)) { free(data); return false; } if (mask & TINY_METADATA_TAGS) { tiny_codec_put_u32(data + offset, (uint32_t)patch->metadata->tag_count); offset += 4; for (index = 0; index < patch->metadata->tag_count; ++index) { if (!delta_put_string(data, capacity, &offset, patch->metadata->tags[index])) { free(data); return false; } } } if (mask & TINY_METADATA_TIER) { data[offset++] = (unsigned char)patch->metadata->memory_tier; } if (mask & TINY_METADATA_KIND) { data[offset++] = (unsigned char)patch->metadata->memory_kind; } if (mask & TINY_METADATA_IMPORTANCE) { data[offset++] = patch->metadata->importance; } if ((mask & TINY_METADATA_SUMMARY) && !delta_put_string(data, capacity, &offset, patch->metadata->summary)) { free(data); return false; } if (mask & TINY_METADATA_RELATIONS) { tiny_codec_put_u32(data + offset, (uint32_t)patch->metadata->relation_count); offset += 4; for (index = 0; index < patch->metadata->relation_count; ++index) { data[offset++] = (unsigned char)patch->metadata->relations[index].type; memcpy(data + offset, patch->metadata->relations[index].message_id, 26); offset += 26; } } if (mask & TINY_METADATA_SOURCES) { tiny_codec_put_u32(data + offset, (uint32_t)patch->metadata->source_count); offset += 4; for (index = 0; index < patch->metadata->source_count; ++index) { memcpy(data + offset, patch->metadata->source_message_ids[index], 26); offset += 26; } } *payload = data; *payload_length = (uint32_t)offset; return true; } static char * delta_get_string(const unsigned char *data, size_t length, size_t *offset, size_t maximum) { uint32_t field_length; char *value; if (*offset > length || length - *offset < 4) { return NULL; } field_length = tiny_codec_get_u32(data + *offset); *offset += 4; if (field_length > maximum || *offset > length || length - *offset < field_length) { return NULL; } value = copy_field(data + *offset, field_length); *offset += field_length; return value; } static tiny_store_status_t prepare_update_payload(tiny_store_t *store, const unsigned char *payload, uint32_t length, tiny_store_entry_t **target, tiny_message_t *prepared) { tiny_message_t holder; tiny_message_patch_t patch; size_t offset = 0, index; uint64_t revision; int64_t updated_at; uint8_t flags; uint32_t mask; char id[27]; tiny_store_status_t status = TINY_STORE_CORRUPT; tiny_message_init(&holder); memset(&patch, 0, sizeof(patch)); #define UPDATE_PAYLOAD_BAD() \ do \ { \ tiny_message_free(&holder); \ return TINY_STORE_CORRUPT; \ } \ while (0) if (length < 47) { UPDATE_PAYLOAD_BAD(); } memcpy(id, payload, 26); id[26] = '\0'; offset = 26; revision = tiny_codec_get_u64(payload + offset); offset += 8; updated_at = (int64_t)tiny_codec_get_u64(payload + offset); offset += 8; flags = payload[offset++]; mask = tiny_codec_get_u32(payload + offset); offset += 4; *target = find_entry(store, id); if (!*target || (*target)->deleted || revision != (*target)->message.revision + 1 || revision > INT32_MAX || !updated_at || (mask & ~TINY_METADATA_ALL) || flags != ((flags & UPDATE_CONTENT ? UPDATE_CONTENT : 0) | (mask ? UPDATE_METADATA : 0))) { UPDATE_PAYLOAD_BAD(); } if (flags & UPDATE_CONTENT) { holder.content = delta_get_string(payload, length, &offset, TINY_CONTENT_MAX); if (!holder.content) { UPDATE_PAYLOAD_BAD(); } patch.content = holder.content; } patch.metadata = &holder.metadata; patch.metadata_mask = mask; if (mask & TINY_METADATA_TOOL_CALL_ID) { holder.metadata.tool_call_id = delta_get_string(payload, length, &offset, TINY_TOOL_CALL_ID_MAX); if (!holder.metadata.tool_call_id) { UPDATE_PAYLOAD_BAD(); } if (!holder.metadata.tool_call_id[0]) { free(holder.metadata.tool_call_id); holder.metadata.tool_call_id = NULL; } } if (mask & TINY_METADATA_TAGS) { uint32_t count; if (offset > length || length - offset < 4) { UPDATE_PAYLOAD_BAD(); } count = tiny_codec_get_u32(payload + offset); offset += 4; if (count > TINY_TAG_COUNT_MAX || (count && !(holder.metadata.tags = calloc(count, sizeof(char *))))) { UPDATE_PAYLOAD_BAD(); } for (index = 0; index < count; ++index) { holder.metadata.tags[index] = delta_get_string(payload, length, &offset, TINY_TAG_MAX); if (!holder.metadata.tags[index] || !holder.metadata.tags[index][0]) { UPDATE_PAYLOAD_BAD(); } holder.metadata.tag_count++; } } if (mask & TINY_METADATA_TIER) { if (offset >= length) { UPDATE_PAYLOAD_BAD(); } holder.metadata.memory_tier = (tiny_memory_tier_t)payload[offset++]; } if (mask & TINY_METADATA_KIND) { if (offset >= length) { UPDATE_PAYLOAD_BAD(); } holder.metadata.memory_kind = (tiny_memory_kind_t)payload[offset++]; } if (mask & TINY_METADATA_IMPORTANCE) { if (offset >= length) { UPDATE_PAYLOAD_BAD(); } holder.metadata.importance = payload[offset++]; } if (mask & TINY_METADATA_SUMMARY) { holder.metadata.summary = delta_get_string(payload, length, &offset, TINY_SUMMARY_MAX); if (!holder.metadata.summary) { UPDATE_PAYLOAD_BAD(); } if (!holder.metadata.summary[0]) { free(holder.metadata.summary); holder.metadata.summary = NULL; } } if (mask & TINY_METADATA_RELATIONS) { uint32_t count; if (offset > length || length - offset < 4) { UPDATE_PAYLOAD_BAD(); } count = tiny_codec_get_u32(payload + offset); offset += 4; if (count > TINY_RELATION_COUNT_MAX || offset > length || length - offset < (size_t)count * 27 || (count && !(holder.metadata.relations = calloc(count, sizeof(tiny_relation_t))))) { UPDATE_PAYLOAD_BAD(); } for (index = 0; index < count; ++index) { holder.metadata.relations[index].type = (tiny_relation_type_t)payload[offset++]; memcpy(holder.metadata.relations[index].message_id, payload + offset, 26); holder.metadata.relations[index].message_id[26] = '\0'; offset += 26; holder.metadata.relation_count++; } } if (mask & TINY_METADATA_SOURCES) { uint32_t count; if (offset > length || length - offset < 4) { UPDATE_PAYLOAD_BAD(); } count = tiny_codec_get_u32(payload + offset); offset += 4; if (count > TINY_SOURCE_COUNT_MAX || offset > length || length - offset < (size_t)count * 26 || (count && !(holder.metadata.source_message_ids = calloc(count, sizeof(char *))))) { UPDATE_PAYLOAD_BAD(); } for (index = 0; index < count; ++index) { holder.metadata.source_message_ids[index] = copy_field(payload + offset, 26); if (!holder.metadata.source_message_ids[index]) { UPDATE_PAYLOAD_BAD(); } offset += 26; holder.metadata.source_count++; } } if (offset != length) { UPDATE_PAYLOAD_BAD(); } status = prepare_update(store, *target, &patch, revision, updated_at, prepared); tiny_message_free(&holder); #undef UPDATE_PAYLOAD_BAD return status; } static void clear_entries(tiny_store_t *store) { size_t index; for (index = 0; index < store->entry_count; ++index) { tiny_message_free(&store->entries[index].message); } store->entry_count = 0; store->last_sequence = 0; } static tiny_store_status_t recover_torn_tail(tiny_store_t *store, uint64_t boundary) { if (boundary > INT64_MAX || !tiny_sys_file_seek(&store->log, (int64_t)boundary, TINY_SYS_SEEK_BEGIN) || !tiny_sys_file_truncate(&store->log) || !tiny_sys_file_flush(&store->log)) { return TINY_STORE_IO_ERROR; } store->recovered_tail = true; set_error(store, "an incomplete trailing record was truncated during recovery"); return TINY_STORE_OK; } static tiny_store_status_t replay_log(tiny_store_t *store, uint64_t starting_offset) { uint64_t file_size, boundary = starting_offset; if (starting_offset > INT64_MAX || !tiny_sys_file_seek(&store->log, (int64_t)starting_offset, TINY_SYS_SEEK_BEGIN) || !tiny_sys_file_size(&store->log, &file_size)) { return TINY_STORE_IO_ERROR; } while (boundary < file_size) { unsigned char header[RECORD_HEADER_SIZE], *payload = NULL; uint32_t actual = 0, payload_length, expected_crc; uint64_t sequence; tiny_store_status_t status = TINY_STORE_OK; if (!read_all(&store->log, header, sizeof(header), &actual)) { return TINY_STORE_IO_ERROR; } if (actual < sizeof(header)) { return recover_torn_tail(store, boundary); } if (memcmp(header, "TCMR", 4) != 0 || (header[4] != RECORD_VERSION && header[4] != RECORD_VERSION_LEGACY) || (header[5] != RECORD_PUT && header[5] != RECORD_DEL && header[5] != RECORD_MARKER && header[5] != RECORD_UPDATE)) { return TINY_STORE_CORRUPT; } payload_length = tiny_codec_get_u32(header + 8); sequence = tiny_codec_get_u64(header + 12); expected_crc = tiny_codec_get_u32(header + 20); if (payload_length > RECORD_PAYLOAD_MAX || (header[5] != RECORD_MARKER && sequence <= store->last_sequence)) { return TINY_STORE_CORRUPT; } payload = malloc(payload_length == 0 ? 1 : payload_length); if (payload == NULL) { return TINY_STORE_NO_MEMORY; } if (!read_all(&store->log, payload, payload_length, &actual)) { free(payload); return TINY_STORE_IO_ERROR; } if (actual < payload_length) { free(payload); return recover_torn_tail(store, boundary); } if (tiny_codec_crc32(payload, payload_length) != expected_crc) { free(payload); return TINY_STORE_CORRUPT; } switch (header[5]) { case RECORD_MARKER: if (boundary != 0 || sequence != 0 || payload_length != sizeof(store->generation)) { free(payload); return TINY_STORE_CORRUPT; } memcpy(store->generation, payload, sizeof(store->generation)); store->has_generation = true; break; case RECORD_PUT: { tiny_message_t message; if (!deserialize_put(payload, payload_length, &message)) { free(payload); return TINY_STORE_CORRUPT; } status = apply_put(store, &message, sequence); tiny_message_free(&message); } break; case RECORD_DEL: { char id[27]; tiny_store_entry_t *entry; if (payload_length != 34) { free(payload); return TINY_STORE_CORRUPT; } memcpy(id, payload, 26); id[26] = '\0'; entry = find_entry(store, id); if (entry == NULL || entry->deleted || !tiny_message_id_valid(id)) { free(payload); return TINY_STORE_CORRUPT; } entry->deleted = true; store->dead_bytes += RECORD_HEADER_SIZE + payload_length; } break; case RECORD_UPDATE: { tiny_store_entry_t *target = NULL; tiny_message_t prepared; tiny_message_init(&prepared); status = prepare_update_payload(store, payload, payload_length, &target, &prepared); if (status == TINY_STORE_OK) { tiny_message_free(&target->message); target->message = prepared; tiny_message_init(&prepared); } tiny_message_free(&prepared); } store->update_records++; store->dead_bytes += RECORD_HEADER_SIZE + payload_length; break; default: free(payload); return TINY_STORE_CORRUPT; } free(payload); if (status != TINY_STORE_OK) { return status; } if (header[5] != RECORD_MARKER) { store->last_sequence = sequence; } boundary += RECORD_HEADER_SIZE + payload_length; } return TINY_STORE_OK; } static tiny_store_status_t write_snapshot(tiny_store_t *store) { char *temporary = tiny_sys_path_join(store->directory, "snapshot.tmp"); tiny_sys_file_t file; unsigned char data[SNAPSHOT_HEADER_SIZE] = { 0 }; uint64_t size; bool ok; size_t index; if (temporary == NULL) { return TINY_STORE_NO_MEMORY; } if (tiny_sys_file_open(&file, temporary, TINY_SYS_FILE_WRITE_NEW) != TINY_SYS_OK) { free(temporary); return TINY_STORE_IO_ERROR; } if (!tiny_sys_file_size(&store->log, &size)) { tiny_sys_file_close(&file); free(temporary); return TINY_STORE_IO_ERROR; } memcpy(data, "TCMS", 4); data[4] = 1; memcpy(data + 8, store->generation, 16); tiny_codec_put_u64(data + 24, store->last_sequence); tiny_codec_put_u64(data + 32, size); tiny_codec_put_u64(data + 40, (uint64_t)store->entry_count); tiny_codec_put_u32(data + 48, tiny_codec_crc32(data, 48)); ok = write_all(&file, data, sizeof(data)); for (index = 0; ok && index < store->entry_count; ++index) { unsigned char prefix[16], *payload = NULL; uint32_t length = 0; if (!prune_metadata_references(store, &store->entries[index].message) || !serialize_put(&store->entries[index].message, &payload, &length)) { ok = false; break; } tiny_codec_put_u64(prefix, store->entries[index].sequence); tiny_codec_put_u32(prefix + 8, length); tiny_codec_put_u32(prefix + 12, tiny_codec_crc32(payload, length)); ok = write_all(&file, prefix, sizeof(prefix)) && write_all(&file, payload, length); free(payload); } if (ok) { ok = tiny_sys_file_flush(&file); } tiny_sys_file_close(&file); if (ok) { ok = tiny_sys_path_replace(temporary, store->snapshot_path); } free(temporary); store->snapshot_valid = ok; return ok ? TINY_STORE_OK : TINY_STORE_IO_ERROR; } static bool log_generation_matches(tiny_store_t *store, const unsigned char generation[16]) { unsigned char header[RECORD_HEADER_SIZE], payload[16]; uint32_t actual = 0; if (!tiny_sys_file_seek(&store->log, 0, TINY_SYS_SEEK_BEGIN) || !read_all(&store->log, header, sizeof(header), &actual) || actual != sizeof(header) || memcmp(header, "TCMR", 4) != 0 || (header[4] != RECORD_VERSION && header[4] != RECORD_VERSION_LEGACY) || header[5] != RECORD_MARKER || tiny_codec_get_u64(header + 12) != 0 || tiny_codec_get_u32(header + 8) != 16 || !read_all(&store->log, payload, sizeof(payload), &actual) || actual != sizeof(payload) || tiny_codec_get_u32(header + 20) != tiny_codec_crc32(payload, sizeof(payload))) { return false; } return memcmp(payload, generation, 16) == 0; } static bool snapshot_invalid(tiny_store_t *store, tiny_sys_file_t *file, bool clear_loaded_entries) { if (clear_loaded_entries) { clear_entries(store); store->has_generation = false; } set_error(store, "snapshot validation failed; the active log was replayed from the beginning"); tiny_sys_file_close(file); return false; } static bool load_snapshot(tiny_store_t *store, uint64_t *log_offset) { tiny_sys_file_t file; unsigned char header[SNAPSHOT_HEADER_SIZE]; uint32_t actual = 0; uint64_t count, index, expected_sequence, offset; uint64_t log_size; if (tiny_sys_file_open(&file, store->snapshot_path, TINY_SYS_FILE_READ) != TINY_SYS_OK) { return false; } if (!read_all(&file, header, sizeof(header), &actual) || actual != sizeof(header) || memcmp(header, "TCMS", 4) != 0 || header[4] != 1 || tiny_codec_get_u32(header + 48) != tiny_codec_crc32(header, 48) || !tiny_sys_file_size(&store->log, &log_size)) { return snapshot_invalid(store, &file, false); } expected_sequence = tiny_codec_get_u64(header + 24); offset = tiny_codec_get_u64(header + 32); count = tiny_codec_get_u64(header + 40); if (offset > log_size || count > SIZE_MAX || !log_generation_matches(store, header + 8)) { return snapshot_invalid(store, &file, false); } memcpy(store->generation, header + 8, 16); store->has_generation = true; for (index = 0; index < count; ++index) { unsigned char prefix[16], *payload; uint32_t length, crc; tiny_message_t message; tiny_store_status_t status; if (!read_all(&file, prefix, sizeof(prefix), &actual) || actual != sizeof(prefix)) { return snapshot_invalid(store, &file, true); } length = tiny_codec_get_u32(prefix + 8); crc = tiny_codec_get_u32(prefix + 12); if (length > RECORD_PAYLOAD_MAX) { return snapshot_invalid(store, &file, true); } payload = malloc(length ? length : 1); if (!payload) { return snapshot_invalid(store, &file, true); } if (!read_all(&file, payload, length, &actual) || actual != length || tiny_codec_crc32(payload, length) != crc || !deserialize_put(payload, length, &message)) { free(payload); return snapshot_invalid(store, &file, true); } free(payload); status = apply_put(store, &message, tiny_codec_get_u64(prefix)); tiny_message_free(&message); if (status != TINY_STORE_OK) { return snapshot_invalid(store, &file, true); } store->last_sequence = tiny_codec_get_u64(prefix); } if (store->last_sequence != expected_sequence) { return snapshot_invalid(store, &file, true); } tiny_sys_file_close(&file); *log_offset = offset; store->snapshot_valid = true; return true; } static tiny_store_status_t store_open_failed(tiny_store_t *store, char *lock_path, tiny_store_status_t status) { free(lock_path); tiny_store_close(store); return status; } tiny_store_status_t tiny_store_open(tiny_store_t **output, const char *directory) { tiny_store_t *store; char *lock_path = NULL; tiny_sys_status_t sys_status; tiny_store_status_t status; if (output == NULL) { return TINY_STORE_IO_ERROR; } *output = NULL; if (directory == NULL || directory[0] == '\0') { return TINY_STORE_IO_ERROR; } store = calloc(1, sizeof(*store)); if (store == NULL) { return TINY_STORE_NO_MEMORY; } tiny_sys_file_init(&store->lock); tiny_sys_file_init(&store->log); store->directory = copy_field((const unsigned char *)directory, strlen(directory)); store->log_path = tiny_sys_path_join(directory, "active.log"); store->snapshot_path = tiny_sys_path_join(directory, "snapshot.bin"); lock_path = tiny_sys_path_join(directory, "store.lock"); if (!store->directory || !store->log_path || !store->snapshot_path || !lock_path) { return store_open_failed(store, lock_path, TINY_STORE_NO_MEMORY); } if (tiny_sys_directory_create(directory) != TINY_SYS_OK) { return store_open_failed(store, lock_path, TINY_STORE_IO_ERROR); } sys_status = tiny_sys_file_open(&store->lock, lock_path, TINY_SYS_FILE_LOCK); free(lock_path); lock_path = NULL; if (sys_status != TINY_SYS_OK) { status = sys_status == TINY_SYS_BUSY ? TINY_STORE_BUSY : TINY_STORE_IO_ERROR; return store_open_failed(store, lock_path, status); } if (tiny_sys_file_open(&store->log, store->log_path, TINY_SYS_FILE_READ_WRITE_CREATE) != TINY_SYS_OK) { return store_open_failed(store, lock_path, TINY_STORE_IO_ERROR); } { uint64_t replay_offset = 0; (void)load_snapshot(store, &replay_offset); status = replay_log(store, replay_offset); } if (status != TINY_STORE_OK) { return store_open_failed(store, lock_path, status); } *output = store; return TINY_STORE_OK; } void tiny_store_close(tiny_store_t *store) { size_t index; if (!store) { return; } tiny_sys_file_close(&store->log); tiny_sys_file_close(&store->lock); for (index = 0; index < store->entry_count; ++index) { tiny_message_free(&store->entries[index].message); } free(store->entries); free(store->directory); free(store->log_path); free(store->snapshot_path); free(store); } const char * tiny_store_status_name(tiny_store_status_t status) { switch (status) { case TINY_STORE_OK: return "OK"; case TINY_STORE_NOT_FOUND: return "NOT_FOUND"; case TINY_STORE_BUSY: return "BUSY"; case TINY_STORE_IO_ERROR: return "IO_ERROR"; case TINY_STORE_CORRUPT: return "CORRUPT_STORE"; case TINY_STORE_CONFLICT: return "CONFLICT"; default: return "INTERNAL_ERROR"; } } tiny_store_status_t tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_t *written) { tiny_message_t message, indexed; unsigned char *payload = NULL; uint32_t length = 0; tiny_store_status_t status; if (written != NULL) { tiny_message_init(written); } if (store == NULL || input == NULL || written == NULL || input->session_id == NULL || input->content == NULL) { return TINY_STORE_IO_ERROR; } if (!input->content[0] || strlen(input->content) > TINY_CONTENT_MAX || !tiny_utf8_valid(input->content, strlen(input->content)) || !metadata_targets_valid(store, NULL, &input->metadata)) { return TINY_STORE_CONFLICT; } tiny_message_init(&message); tiny_message_init(&indexed); if (tiny_message_copy(&message, input) != 0) { return TINY_STORE_NO_MEMORY; } message.timestamp_ms = (int64_t)now_milliseconds(); message.updated_at_ms = message.timestamp_ms; message.revision = 1; make_ulid(store, (uint64_t)message.timestamp_ms, message.message_id); if (!serialize_put(&message, &payload, &length) || !ensure_entries(store) || tiny_message_copy(&indexed, &message) != 0) { free(payload); tiny_message_free(&message); tiny_message_free(&indexed); return TINY_STORE_NO_MEMORY; } status = append_record_to(&store->log, RECORD_PUT, store->last_sequence + 1, payload, length, true); free(payload); if (status != TINY_STORE_OK) { tiny_message_free(&message); tiny_message_free(&indexed); set_error(store, "failed to append and flush a PUT record"); return status; } store->last_sequence++; memset(&store->entries[store->entry_count], 0, sizeof(store->entries[store->entry_count])); store->entries[store->entry_count].message = indexed; store->entries[store->entry_count].sequence = store->last_sequence; store->entry_count++; *written = message; return TINY_STORE_OK; } tiny_store_status_t tiny_store_update(tiny_store_t *store, const char *message_id, const tiny_message_patch_t *patch, tiny_message_t *updated, bool *changed) { tiny_store_entry_t *entry; tiny_message_t prepared, response; tiny_message_patch_t effective; unsigned char *payload = NULL; uint32_t payload_length = 0; uint64_t log_size_before = 0; uint64_t revision; int64_t updated_at; tiny_store_status_t status; if (!store || !message_id || !patch || !updated || !changed || !tiny_message_id_valid(message_id)) { return TINY_STORE_NOT_FOUND; } tiny_message_init(updated); tiny_message_init(&prepared); tiny_message_init(&response); *changed = false; entry = find_entry(store, message_id); if (!entry || entry->deleted) { return TINY_STORE_NOT_FOUND; } if (patch->has_expected_revision && patch->expected_revision != entry->message.revision) { return TINY_STORE_CONFLICT; } if (entry->message.revision >= INT32_MAX) { return TINY_STORE_CONFLICT; } effective = *patch; if ((effective.metadata_mask & ~TINY_METADATA_ALL) != 0 || (effective.metadata_mask && (!effective.metadata || !tiny_metadata_valid(effective.metadata)))) { return TINY_STORE_CONFLICT; } if (effective.content && strcmp(effective.content, entry->message.content) == 0) { effective.content = NULL; } if (!effective.metadata || effective.metadata_mask == 0 || !metadata_patch_changes( &entry->message.metadata, effective.metadata, effective.metadata_mask)) { effective.metadata_mask = 0; } if (!effective.content && effective.metadata_mask == 0) { return tiny_message_copy(updated, &entry->message) == 0 ? TINY_STORE_OK : TINY_STORE_NO_MEMORY; } revision = entry->message.revision + 1; updated_at = (int64_t)now_milliseconds(); status = prepare_update(store, entry, &effective, revision, updated_at, &prepared); if (status != TINY_STORE_OK) { tiny_message_free(&prepared); return status; } if (tiny_message_copy(&response, &prepared) != 0) { status = TINY_STORE_NO_MEMORY; tiny_message_free(&prepared); return status; } if (!serialize_update(entry, &effective, revision, updated_at, &payload, &payload_length)) { status = TINY_STORE_NO_MEMORY; tiny_message_free(&prepared); tiny_message_free(&response); return status; } if (!tiny_sys_file_size(&store->log, &log_size_before)) { free(payload); tiny_message_free(&prepared); tiny_message_free(&response); return TINY_STORE_IO_ERROR; } status = append_record_to( &store->log, RECORD_UPDATE, store->last_sequence + 1, payload, payload_length, true); if (status != TINY_STORE_OK) { bool rolled_back = log_size_before <= INT64_MAX && tiny_sys_file_seek(&store->log, (int64_t)log_size_before, TINY_SYS_SEEK_BEGIN) && tiny_sys_file_truncate(&store->log) && tiny_sys_file_flush(&store->log); set_error(store, "failed to append and flush an UPDATE record"); if (!rolled_back) { set_error(store, "UPDATE failed and its incomplete record could not be rolled back"); } free(payload); tiny_message_free(&prepared); tiny_message_free(&response); return status; } tiny_message_free(&entry->message); entry->message = prepared; tiny_message_init(&prepared); *updated = response; tiny_message_init(&response); store->last_sequence++; store->update_records++; store->dead_bytes += RECORD_HEADER_SIZE + payload_length; *changed = true; free(payload); tiny_message_free(&prepared); tiny_message_free(&response); return TINY_STORE_OK; } tiny_store_status_t tiny_store_delete(tiny_store_t *store, const char *message_id, const char *session_id) { tiny_store_entry_t *entry; unsigned char payload[34]; tiny_store_status_t status; if (store == NULL || message_id == NULL || !tiny_message_id_valid(message_id)) { return TINY_STORE_NOT_FOUND; } entry = find_entry(store, message_id); if (!entry || entry->deleted || (session_id && strcmp(entry->message.session_id, session_id) != 0)) { return TINY_STORE_NOT_FOUND; } memcpy(payload, message_id, 26); tiny_codec_put_u64(payload + 26, now_milliseconds()); status = append_record_to( &store->log, RECORD_DEL, store->last_sequence + 1, payload, sizeof(payload), true); if (status != TINY_STORE_OK) { set_error(store, "failed to append and flush a DEL record"); return status; } store->last_sequence++; entry->deleted = true; store->dead_bytes += RECORD_HEADER_SIZE + sizeof(payload); return TINY_STORE_OK; } tiny_store_status_t tiny_store_rebuild(tiny_store_t *store, tiny_rebuild_report_t *report) { char *temporary; tiny_sys_file_t file; uint64_t before = 0, after = 0; size_t read_index, write_index = 0; uint64_t removed = 0, max_sequence = 0; tiny_store_status_t status = TINY_STORE_OK; unsigned char previous_generation[sizeof(store->generation)]; bool previous_has_generation; bool log_replaced = false; if (report != NULL) { memset(report, 0, sizeof(*report)); } if (store == NULL || report == NULL) { return TINY_STORE_IO_ERROR; } if (store->snapshot_valid && store->update_records == 0 && store->dead_bytes == 0) { uint64_t size = 0; (void)tiny_sys_file_size(&store->log, &size); report->messages_indexed = store->entry_count; report->bytes_before = size; report->bytes_after = size; return TINY_STORE_OK; } memcpy(previous_generation, store->generation, sizeof(previous_generation)); previous_has_generation = store->has_generation; temporary = tiny_sys_path_join(store->directory, "active.tmp"); if (!temporary) { return TINY_STORE_NO_MEMORY; } if (!tiny_sys_file_size(&store->log, &before) || tiny_sys_file_open(&file, temporary, TINY_SYS_FILE_WRITE_NEW) != TINY_SYS_OK) { free(temporary); return TINY_STORE_IO_ERROR; } if (!tiny_sys_random_bytes(store->generation, sizeof(store->generation))) { tiny_sys_file_close(&file); free(temporary); return TINY_STORE_IO_ERROR; } store->has_generation = true; status = append_record_to( &file, RECORD_MARKER, 0, store->generation, sizeof(store->generation), false); for (read_index = 0; read_index < store->entry_count; ++read_index) { if (status == TINY_STORE_OK && !store->entries[read_index].deleted) { unsigned char *payload = NULL; uint32_t length = 0; if (!prune_metadata_references(store, &store->entries[read_index].message) || !serialize_put(&store->entries[read_index].message, &payload, &length)) { status = TINY_STORE_NO_MEMORY; break; } status = append_record_to( &file, RECORD_PUT, store->entries[read_index].sequence, payload, length, false); free(payload); if (status != TINY_STORE_OK) { break; } if (store->entries[read_index].sequence > max_sequence) { max_sequence = store->entries[read_index].sequence; } } else { removed++; } } if (status == TINY_STORE_OK && !tiny_sys_file_flush(&file)) { status = TINY_STORE_IO_ERROR; } tiny_sys_file_close(&file); if (status == TINY_STORE_OK) { tiny_sys_file_close(&store->log); if (!tiny_sys_path_replace(temporary, store->log_path)) { status = TINY_STORE_IO_ERROR; } else { log_replaced = true; } if (tiny_sys_file_open(&store->log, store->log_path, TINY_SYS_FILE_READ_WRITE_CREATE) != TINY_SYS_OK) { status = TINY_STORE_IO_ERROR; } } if (status == TINY_STORE_OK) { for (read_index = 0; read_index < store->entry_count; ++read_index) { if (store->entries[read_index].deleted) { tiny_message_free(&store->entries[read_index].message); } else { if (write_index != read_index) { store->entries[write_index] = store->entries[read_index]; } write_index++; } } store->entry_count = write_index; store->last_sequence = max_sequence; (void)tiny_sys_file_size(&store->log, &after); report->messages_indexed = write_index; report->records_removed = removed; report->bytes_before = before; report->bytes_after = after; status = write_snapshot(store); if (status == TINY_STORE_OK) { store->update_records = 0; store->dead_bytes = 0; } } if (status != TINY_STORE_OK) { if (!log_replaced) { memcpy(store->generation, previous_generation, sizeof(store->generation)); store->has_generation = previous_has_generation; } set_error(store, "compaction did not complete; the previous valid log is retained when replacement " "had not begun"); } free(temporary); return status; } void tiny_store_health(const tiny_store_t *store, tiny_store_health_t *health) { uint64_t size; size_t index; if (health == NULL) { return; } memset(health, 0, sizeof(*health)); if (store == NULL) { return; } health->format_version = RECORD_VERSION; if (tiny_sys_file_size(&store->log, &size)) { health->log_size = size; } { tiny_sys_file_t file; if (tiny_sys_file_open(&file, store->snapshot_path, TINY_SYS_FILE_READ) == TINY_SYS_OK) { if (tiny_sys_file_size(&file, &size)) { health->snapshot_size = size; } tiny_sys_file_close(&file); } } for (index = 0; index < store->entry_count; ++index) { const tiny_message_t *message = &store->entries[index].message; size_t item; health->memory_entry_bytes += sizeof(store->entries[index]); health->memory_body_bytes += message->session_id ? strlen(message->session_id) + 1U : 0; health->memory_body_bytes += message->content ? strlen(message->content) + 1U : 0; health->memory_metadata_bytes += message->metadata.tool_call_id ? strlen(message->metadata.tool_call_id) + 1U : 0; health->memory_metadata_bytes += message->metadata.summary ? strlen(message->metadata.summary) + 1U : 0; health->memory_metadata_bytes += message->metadata.relation_count * sizeof(*message->metadata.relations); for (item = 0; item < message->metadata.tag_count; ++item) { health->memory_metadata_bytes += strlen(message->metadata.tags[item]) + 1U; } health->memory_metadata_bytes += message->metadata.tag_count * sizeof(*message->metadata.tags); health->memory_metadata_bytes += message->metadata.source_count * sizeof(*message->metadata.source_message_ids); for (item = 0; item < message->metadata.source_count; ++item) { health->memory_metadata_bytes += strlen(message->metadata.source_message_ids[item]) + 1U; } if (store->entries[index].deleted) { health->deleted_messages++; } else { health->active_messages++; } } health->last_sequence = store->last_sequence; health->update_records = store->update_records; health->dead_bytes = store->dead_bytes; { tiny_sys_io_counters_t counters; tiny_sys_io_counters_get(&counters); health->write_bytes = counters.write_bytes; health->write_calls = counters.write_calls; health->flush_calls = counters.flush_calls; health->truncate_calls = counters.truncate_calls; health->replace_calls = counters.replace_calls; } health->recovered_tail = store->recovered_tail; health->snapshot_valid = store->snapshot_valid; snprintf(health->last_error, sizeof(health->last_error), "%s", store->last_error); } tiny_store_status_t tiny_store_import(tiny_store_t *store, const tiny_import_record_t *records, size_t record_count, tiny_import_policy_t policy, size_t *imported_count, size_t *rejected_count) { tiny_store_entry_t *saved_entries; size_t saved_count, saved_capacity, index; tiny_rebuild_report_t report; tiny_store_status_t status = TINY_STORE_OK; if (!store || !records || !imported_count || !rejected_count) { return TINY_STORE_IO_ERROR; } *imported_count = 0; *rejected_count = 0; saved_entries = store->entries; saved_count = store->entry_count; saved_capacity = store->entry_capacity; store->entries = NULL; store->entry_count = 0; store->entry_capacity = 0; for (index = 0; index < record_count && status == TINY_STORE_OK; ++index) { const tiny_import_record_t *record = &records[index]; tiny_store_entry_t *existing = find_entry(store, record->message.message_id); if (existing != NULL && policy == TINY_IMPORT_REJECT && !record->deleted && !existing->deleted) { (*rejected_count)++; continue; } if (existing != NULL) { tiny_message_t replacement; tiny_message_init(&replacement); if (tiny_message_copy(&replacement, &record->message) != 0) { status = TINY_STORE_NO_MEMORY; break; } tiny_message_free(&existing->message); existing->deleted = record->deleted; existing->sequence = (uint64_t)record->message.timestamp_ms; existing->message = replacement; (*imported_count)++; } else { tiny_message_t copied; tiny_message_init(&copied); if (!ensure_entries(store) || tiny_message_copy(&copied, &record->message) != 0) { tiny_message_free(&copied); status = TINY_STORE_NO_MEMORY; break; } memset( &store->entries[store->entry_count], 0, sizeof(store->entries[store->entry_count])); store->entries[store->entry_count].deleted = record->deleted; store->entries[store->entry_count].sequence = (uint64_t)record->message.timestamp_ms; store->entries[store->entry_count].message = copied; store->entry_count++; (*imported_count)++; } } if (status == TINY_STORE_OK) { store->update_records = 0; store->dead_bytes = 0; store->snapshot_valid = false; store->recovered_tail = false; status = tiny_store_rebuild(store, &report); if (status != TINY_STORE_OK) { for (index = 0; index < store->entry_count; ++index) { tiny_message_free(&store->entries[index].message); } free(store->entries); store->entries = saved_entries; store->entry_count = saved_count; store->entry_capacity = saved_capacity; } else { for (index = 0; index < saved_count; ++index) { tiny_message_free(&saved_entries[index].message); } free(saved_entries); } } else { for (index = 0; index < store->entry_count; ++index) { tiny_message_free(&store->entries[index].message); } free(store->entries); store->entries = saved_entries; store->entry_count = saved_count; store->entry_capacity = saved_capacity; } return status; } size_t tiny_store_entry_count(const tiny_store_t *store) { return store == NULL ? 0 : store->entry_count; } const tiny_store_entry_t * tiny_store_entry(const tiny_store_t *store, size_t index) { return store != NULL && index < store->entry_count ? &store->entries[index] : NULL; }