summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt2
-rw-r--r--src/domain/message.c176
-rw-r--r--src/domain/message.h52
-rw-r--r--src/domain/search.c26
-rw-r--r--src/mcp/json.c13
-rw-r--r--src/mcp/json.h1
-rw-r--r--src/mcp/tools/common/common.c36
-rw-r--r--src/mcp/tools/common/common.h3
-rw-r--r--src/mcp/tools/common/context.c34
-rw-r--r--src/mcp/tools/common/context.h4
-rw-r--r--src/mcp/tools/common/history.c23
-rw-r--r--src/mcp/tools/common/history.h6
-rw-r--r--src/mcp/tools/common/memory_index.c219
-rw-r--r--src/mcp/tools/common/memory_index.h24
-rw-r--r--src/mcp/tools/common/registry.c5
-rw-r--r--src/mcp/tools/common/registry.h1
-rw-r--r--src/mcp/tools/delete_message.c4
-rw-r--r--src/mcp/tools/get_system_health.c19
-rw-r--r--src/mcp/tools/read_messages.c543
-rw-r--r--src/mcp/tools/rebuild_index.c19
-rw-r--r--src/mcp/tools/search_messages.c134
-rw-r--r--src/mcp/tools/update_message.c97
-rw-r--r--src/mcp/tools/write_message.c212
-rw-r--r--src/storage/store.c856
-rw-r--r--src/storage/store.h25
-rw-r--r--src/sys/system.c59
-rw-r--r--src/sys/system.h12
27 files changed, 2537 insertions, 68 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 1998150..31a5338 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -30,12 +30,14 @@ add_executable(tiny-convmem-mcp
mcp/tools/common/common.c
mcp/tools/common/context.c
mcp/tools/common/history.c
+ mcp/tools/common/memory_index.c
mcp/tools/common/registry.c
mcp/tools/delete_message.c
mcp/tools/get_system_health.c
mcp/tools/read_messages.c
mcp/tools/rebuild_index.c
mcp/tools/search_messages.c
+ mcp/tools/update_message.c
mcp/tools/write_message.c
)
diff --git a/src/domain/message.c b/src/domain/message.c
index b1db3e1..ec019c4 100644
--- a/src/domain/message.c
+++ b/src/domain/message.c
@@ -33,6 +33,9 @@ tiny_message_init(tiny_message_t *message)
}
memset(message, 0, sizeof(*message));
message->metadata.memory_tier = TINY_TIER_SHORT;
+ message->metadata.memory_kind = TINY_KIND_CONVERSATION;
+ message->metadata.importance = 50;
+ message->revision = 1;
}
void
@@ -46,11 +49,18 @@ tiny_message_free(tiny_message_t *message)
free(message->session_id);
free(message->content);
free(message->metadata.tool_call_id);
+ free(message->metadata.summary);
for (index = 0; index < message->metadata.tag_count; ++index)
{
free(message->metadata.tags[index]);
}
free(message->metadata.tags);
+ free(message->metadata.relations);
+ for (index = 0; index < message->metadata.source_count; ++index)
+ {
+ free(message->metadata.source_message_ids[index]);
+ }
+ free(message->metadata.source_message_ids);
tiny_message_init(message);
}
@@ -59,8 +69,7 @@ tiny_message_copy(tiny_message_t *destination, const tiny_message_t *source)
{
size_t index;
if (destination == NULL || source == NULL || destination == source || source->session_id == NULL
- || source->content == NULL
- || (source->metadata.tag_count != 0 && source->metadata.tags == NULL))
+ || source->content == NULL || !tiny_metadata_valid(&source->metadata))
{
return -1;
}
@@ -69,12 +78,56 @@ tiny_message_copy(tiny_message_t *destination, const tiny_message_t *source)
destination->timestamp_ms = source->timestamp_ms;
destination->role = source->role;
destination->metadata.memory_tier = source->metadata.memory_tier;
+ destination->metadata.memory_kind = source->metadata.memory_kind;
+ destination->metadata.importance = source->metadata.importance;
+ destination->revision = source->revision;
+ destination->updated_at_ms = source->updated_at_ms;
destination->session_id = duplicate_string(source->session_id);
destination->content = duplicate_string(source->content);
if (destination->session_id == NULL || destination->content == NULL)
{
return copy_failed(destination);
}
+ if (source->metadata.summary != NULL)
+ {
+ destination->metadata.summary = duplicate_string(source->metadata.summary);
+ if (destination->metadata.summary == NULL)
+ {
+ return copy_failed(destination);
+ }
+ }
+ if (source->metadata.relation_count > 0)
+ {
+ destination->metadata.relations
+ = malloc(source->metadata.relation_count * sizeof(*destination->metadata.relations));
+ if (destination->metadata.relations == NULL)
+ {
+ return copy_failed(destination);
+ }
+ memcpy(destination->metadata.relations,
+ source->metadata.relations,
+ source->metadata.relation_count * sizeof(*destination->metadata.relations));
+ destination->metadata.relation_count = source->metadata.relation_count;
+ }
+ if (source->metadata.source_count > 0)
+ {
+ destination->metadata.source_message_ids
+ = calloc(source->metadata.source_count, sizeof(char *));
+ if (destination->metadata.source_message_ids == NULL)
+ {
+ return copy_failed(destination);
+ }
+ for (index = 0; index < source->metadata.source_count; ++index)
+ {
+ destination->metadata.source_message_ids[index]
+ = duplicate_string(source->metadata.source_message_ids[index]);
+ if (destination->metadata.source_message_ids[index] == NULL)
+ {
+ return copy_failed(destination);
+ }
+ destination->metadata.source_count++;
+ }
+ }
if (source->metadata.tool_call_id != NULL)
{
destination->metadata.tool_call_id = duplicate_string(source->metadata.tool_call_id);
@@ -103,6 +156,70 @@ tiny_message_copy(tiny_message_t *destination, const tiny_message_t *source)
return 0;
}
+bool
+tiny_metadata_valid(const tiny_metadata_t *metadata)
+{
+ size_t index, other;
+ if (!metadata || metadata->memory_tier < TINY_TIER_SHORT
+ || metadata->memory_tier > TINY_TIER_DISCARD || metadata->memory_kind < TINY_KIND_CONVERSATION
+ || metadata->memory_kind > TINY_KIND_SUMMARY || metadata->importance > 100
+ || metadata->tag_count > TINY_TAG_COUNT_MAX || (metadata->tag_count && !metadata->tags)
+ || metadata->relation_count > TINY_RELATION_COUNT_MAX
+ || (metadata->relation_count && !metadata->relations)
+ || metadata->source_count > TINY_SOURCE_COUNT_MAX
+ || (metadata->source_count && !metadata->source_message_ids))
+ {
+ return false;
+ }
+ if (metadata->summary
+ && (strlen(metadata->summary) > TINY_SUMMARY_MAX
+ || !tiny_utf8_valid(metadata->summary, strlen(metadata->summary))))
+ {
+ return false;
+ }
+ for (index = 0; index < metadata->tag_count; ++index)
+ {
+ if (!metadata->tags[index] || !metadata->tags[index][0]
+ || strlen(metadata->tags[index]) > TINY_TAG_MAX)
+ {
+ return false;
+ }
+ }
+ for (index = 0; index < metadata->relation_count; ++index)
+ {
+ const tiny_relation_t *relation = &metadata->relations[index];
+ if (relation->type < TINY_RELATION_DEPENDS_ON || relation->type > TINY_RELATION_RELATED_TO
+ || !tiny_message_id_valid(relation->message_id))
+ {
+ return false;
+ }
+ for (other = 0; other < index; ++other)
+ {
+ if (relation->type == metadata->relations[other].type
+ && strcmp(relation->message_id, metadata->relations[other].message_id) == 0)
+ {
+ return false;
+ }
+ }
+ }
+ for (index = 0; index < metadata->source_count; ++index)
+ {
+ if (!metadata->source_message_ids[index]
+ || !tiny_message_id_valid(metadata->source_message_ids[index]))
+ {
+ return false;
+ }
+ for (other = 0; other < index; ++other)
+ {
+ if (strcmp(metadata->source_message_ids[index], metadata->source_message_ids[other]) == 0)
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
const char *
tiny_role_name(tiny_role_t role)
{
@@ -193,6 +310,61 @@ tiny_tier_parse(const char *value, tiny_memory_tier_t *tier)
return true;
}
+const char *
+tiny_kind_name(tiny_memory_kind_t kind)
+{
+ static const char *names[]
+ = { "unknown", "conversation", "episode", "semantic", "procedure", "preference", "summary" };
+ return kind >= TINY_KIND_CONVERSATION && kind <= TINY_KIND_SUMMARY ? names[kind] : names[0];
+}
+
+bool
+tiny_kind_parse(const char *value, tiny_memory_kind_t *kind)
+{
+ tiny_memory_kind_t candidate;
+ if (value == NULL || kind == NULL)
+ {
+ return false;
+ }
+ for (candidate = TINY_KIND_CONVERSATION; candidate <= TINY_KIND_SUMMARY; ++candidate)
+ {
+ if (strcmp(value, tiny_kind_name(candidate)) == 0)
+ {
+ *kind = candidate;
+ return true;
+ }
+ }
+ return false;
+}
+
+const char *
+tiny_relation_type_name(tiny_relation_type_t type)
+{
+ static const char *names[]
+ = { "unknown", "depends_on", "derived_from", "supersedes", "related_to" };
+ return type >= TINY_RELATION_DEPENDS_ON && type <= TINY_RELATION_RELATED_TO ? names[type]
+ : names[0];
+}
+
+bool
+tiny_relation_type_parse(const char *value, tiny_relation_type_t *type)
+{
+ tiny_relation_type_t candidate;
+ if (value == NULL || type == NULL)
+ {
+ return false;
+ }
+ for (candidate = TINY_RELATION_DEPENDS_ON; candidate <= TINY_RELATION_RELATED_TO; ++candidate)
+ {
+ if (strcmp(value, tiny_relation_type_name(candidate)) == 0)
+ {
+ *type = candidate;
+ return true;
+ }
+ }
+ return false;
+}
+
bool
tiny_utf8_valid(const char *value, size_t length)
{
diff --git a/src/domain/message.h b/src/domain/message.h
index fa36d2a..cece77c 100644
--- a/src/domain/message.h
+++ b/src/domain/message.h
@@ -12,6 +12,9 @@
#define TINY_TAG_MAX 128
#define TINY_TAG_COUNT_MAX 32
#define TINY_QUERY_MAX 4096
+#define TINY_SUMMARY_MAX 4096
+#define TINY_RELATION_COUNT_MAX 32
+#define TINY_SOURCE_COUNT_MAX 64
typedef enum tiny_role
{
@@ -28,14 +31,56 @@ typedef enum tiny_memory_tier
TINY_TIER_DISCARD = 4
} tiny_memory_tier_t;
+typedef enum tiny_memory_kind
+{
+ TINY_KIND_CONVERSATION = 1,
+ TINY_KIND_EPISODE,
+ TINY_KIND_SEMANTIC,
+ TINY_KIND_PROCEDURE,
+ TINY_KIND_PREFERENCE,
+ TINY_KIND_SUMMARY
+} tiny_memory_kind_t;
+
+typedef enum tiny_relation_type
+{
+ TINY_RELATION_DEPENDS_ON = 1,
+ TINY_RELATION_DERIVED_FROM,
+ TINY_RELATION_SUPERSEDES,
+ TINY_RELATION_RELATED_TO
+} tiny_relation_type_t;
+
+typedef struct tiny_relation
+{
+ tiny_relation_type_t type;
+ char message_id[TINY_MESSAGE_ID_LENGTH + 1];
+} tiny_relation_t;
+
typedef struct tiny_metadata
{
char *tool_call_id;
char **tags;
size_t tag_count;
tiny_memory_tier_t memory_tier;
+ tiny_memory_kind_t memory_kind;
+ uint8_t importance;
+ char *summary;
+ tiny_relation_t *relations;
+ size_t relation_count;
+ char **source_message_ids;
+ size_t source_count;
} tiny_metadata_t;
+typedef uint32_t tiny_metadata_mask_t;
+#define TINY_METADATA_TOOL_CALL_ID UINT32_C(0x01)
+#define TINY_METADATA_TAGS UINT32_C(0x02)
+#define TINY_METADATA_TIER UINT32_C(0x04)
+#define TINY_METADATA_KIND UINT32_C(0x08)
+#define TINY_METADATA_IMPORTANCE UINT32_C(0x10)
+#define TINY_METADATA_SUMMARY UINT32_C(0x20)
+#define TINY_METADATA_RELATIONS UINT32_C(0x40)
+#define TINY_METADATA_SOURCES UINT32_C(0x80)
+#define TINY_METADATA_ALL UINT32_C(0xff)
+
typedef struct tiny_message
{
char message_id[TINY_MESSAGE_ID_LENGTH + 1];
@@ -44,6 +89,8 @@ typedef struct tiny_message
tiny_role_t role;
char *content;
tiny_metadata_t metadata;
+ uint64_t revision;
+ int64_t updated_at_ms;
} tiny_message_t;
void tiny_message_init(tiny_message_t *message);
@@ -54,7 +101,12 @@ const char *tiny_role_name(tiny_role_t role);
bool tiny_role_parse(const char *value, tiny_role_t *role);
const char *tiny_tier_name(tiny_memory_tier_t tier);
bool tiny_tier_parse(const char *value, tiny_memory_tier_t *tier);
+const char *tiny_kind_name(tiny_memory_kind_t kind);
+bool tiny_kind_parse(const char *value, tiny_memory_kind_t *kind);
+const char *tiny_relation_type_name(tiny_relation_type_t type);
+bool tiny_relation_type_parse(const char *value, tiny_relation_type_t *type);
bool tiny_utf8_valid(const char *value, size_t length);
bool tiny_message_id_valid(const char *value);
+bool tiny_metadata_valid(const tiny_metadata_t *metadata);
#endif
diff --git a/src/domain/search.c b/src/domain/search.c
index 0f9179d..10109a7 100644
--- a/src/domain/search.c
+++ b/src/domain/search.c
@@ -458,6 +458,12 @@ tiny_search_index_add(tiny_search_index_t *index,
tiny_search_index_free(delta);
return false;
}
+ if (message->metadata.summary
+ && !index_text_direct(delta, message->metadata.summary, message_index))
+ {
+ tiny_search_index_free(delta);
+ return false;
+ }
for (tag = 0; tag < message->metadata.tag_count; ++tag)
{
if (!index_text_direct(delta, message->metadata.tags[tag], message_index))
@@ -609,6 +615,26 @@ tiny_search_score(const char *query, const tiny_message_t *message)
}
free(tag_points);
}
+ if (!found && message->metadata.summary)
+ {
+ uint32_t *summary_points = NULL;
+ size_t summary_count = 0;
+ if (decode_codepoints(message->metadata.summary, &summary_points, &summary_count))
+ {
+ for (message_offset = 0; message_offset + width <= summary_count; ++message_offset)
+ {
+ if (memcmp(query_points + query_offset,
+ summary_points + message_offset,
+ width * sizeof(uint32_t))
+ == 0)
+ {
+ found = true;
+ break;
+ }
+ }
+ }
+ free(summary_points);
+ }
if (found)
{
matches++;
diff --git a/src/mcp/json.c b/src/mcp/json.c
index 62f31c9..59e2395 100644
--- a/src/mcp/json.c
+++ b/src/mcp/json.c
@@ -1,6 +1,7 @@
#include "mcp/json.h"
#include <stdio.h>
+#include <math.h>
#include <stdlib.h>
#include <string.h>
@@ -172,3 +173,15 @@ tiny_json_i64(tiny_json_buffer_t *buffer, long long value)
snprintf(text, sizeof(text), "%lld", value);
return tiny_json_raw(buffer, text);
}
+
+bool
+tiny_json_number(tiny_json_buffer_t *buffer, double value)
+{
+ char text[32];
+ if (!isfinite(value))
+ {
+ return false;
+ }
+ snprintf(text, sizeof(text), "%.17g", value);
+ return tiny_json_raw(buffer, text);
+}
diff --git a/src/mcp/json.h b/src/mcp/json.h
index a46e6c7..fe38a81 100644
--- a/src/mcp/json.h
+++ b/src/mcp/json.h
@@ -19,5 +19,6 @@ bool tiny_json_raw_n(tiny_json_buffer_t *buffer, const char *value, size_t lengt
bool tiny_json_string(tiny_json_buffer_t *buffer, const char *value);
bool tiny_json_u64(tiny_json_buffer_t *buffer, unsigned long long value);
bool tiny_json_i64(tiny_json_buffer_t *buffer, long long value);
+bool tiny_json_number(tiny_json_buffer_t *buffer, double value);
#endif
diff --git a/src/mcp/tools/common/common.c b/src/mcp/tools/common/common.c
index d6669f8..62f507f 100644
--- a/src/mcp/tools/common/common.c
+++ b/src/mcp/tools/common/common.c
@@ -68,6 +68,10 @@ tiny_mcp_tool_append_message(tiny_json_buffer_t *json, const tiny_message_t *mes
tiny_json_string(json, tiny_role_name(message->role));
tiny_json_raw(json, ",\"content\":");
tiny_json_string(json, message->content);
+ tiny_json_raw(json, ",\"revision\":");
+ tiny_json_u64(json, message->revision);
+ tiny_json_raw(json, ",\"updated_at\":");
+ tiny_json_i64(json, message->updated_at_ms);
tiny_json_raw(json, ",\"metadata\":{");
if (message->metadata.tool_call_id)
{
@@ -86,6 +90,38 @@ tiny_mcp_tool_append_message(tiny_json_buffer_t *json, const tiny_message_t *mes
}
tiny_json_raw(json, "],\"memory_tier\":");
tiny_json_string(json, tiny_tier_name(message->metadata.memory_tier));
+ tiny_json_raw(json, ",\"memory_kind\":");
+ tiny_json_string(json, tiny_kind_name(message->metadata.memory_kind));
+ tiny_json_raw(json, ",\"importance\":");
+ tiny_json_u64(json, message->metadata.importance);
+ if (message->metadata.summary)
+ {
+ tiny_json_raw(json, ",\"summary\":");
+ tiny_json_string(json, message->metadata.summary);
+ }
+ tiny_json_raw(json, ",\"relations\":[");
+ for (index = 0; index < message->metadata.relation_count; ++index)
+ {
+ if (index)
+ {
+ tiny_json_raw(json, ",");
+ }
+ tiny_json_raw(json, "{\"type\":");
+ tiny_json_string(json, tiny_relation_type_name(message->metadata.relations[index].type));
+ tiny_json_raw(json, ",\"message_id\":");
+ tiny_json_string(json, message->metadata.relations[index].message_id);
+ tiny_json_raw(json, "}");
+ }
+ tiny_json_raw(json, "],\"source_message_ids\":[");
+ for (index = 0; index < message->metadata.source_count; ++index)
+ {
+ if (index)
+ {
+ tiny_json_raw(json, ",");
+ }
+ tiny_json_string(json, message->metadata.source_message_ids[index]);
+ }
+ tiny_json_raw(json, "]");
tiny_json_raw(json, "}}");
}
diff --git a/src/mcp/tools/common/common.h b/src/mcp/tools/common/common.h
index 07caea1..741b2ce 100644
--- a/src/mcp/tools/common/common.h
+++ b/src/mcp/tools/common/common.h
@@ -3,12 +3,15 @@
#include "domain/message.h"
#include "mcp/json.h"
+#include "domain/message.h"
#include <mcpc/mcpc.h>
void tiny_mcp_tool_add_error(mcpc_ucbr_t *result, const char *code, const char *message);
void tiny_mcp_tool_add_json(mcpc_ucbr_t *result, tiny_json_buffer_t *json);
char *tiny_mcp_tool_get_string(const mcpc_tool_t *tool, const char *name, size_t maximum);
void tiny_mcp_tool_append_message(tiny_json_buffer_t *json, const tiny_message_t *message);
+bool
+tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata_mask_t *present);
mcpc_tool_t *tiny_mcp_tool_new(const char *name, const char *description, mcpc_tcallcb_t callback);
bool tiny_mcp_tool_add_property(mcpc_tool_t *tool,
diff --git a/src/mcp/tools/common/context.c b/src/mcp/tools/common/context.c
index d79b5de..e833f70 100644
--- a/src/mcp/tools/common/context.c
+++ b/src/mcp/tools/common/context.c
@@ -43,10 +43,12 @@ tiny_mcp_tools_context_new(tiny_store_t *store)
context->store = store;
context->search_index = tiny_mcp_tools_build_search_index(store);
context->history_index = tiny_history_index_build(store);
- if (!context->search_index || !context->history_index)
+ context->memory_index = tiny_memory_index_build(store);
+ if (!context->search_index || !context->history_index || !context->memory_index)
{
tiny_search_index_free(context->search_index);
tiny_history_index_free(context->history_index);
+ tiny_memory_index_free(context->memory_index);
free(context);
return NULL;
}
@@ -62,10 +64,40 @@ tiny_mcp_tools_context_free(tiny_mcp_tools_context_t *context)
}
tiny_search_index_free(context->search_index);
tiny_history_index_free(context->history_index);
+ tiny_memory_index_free(context->memory_index);
free(context);
}
bool
+tiny_mcp_tools_ensure_memory_index(tiny_mcp_tools_context_t *context)
+{
+ if (!context)
+ {
+ return false;
+ }
+ if (!context->memory_index)
+ {
+ context->memory_index = tiny_memory_index_build(context->store);
+ }
+ return context->memory_index != NULL;
+}
+
+void
+tiny_mcp_tools_invalidate_indexes(tiny_mcp_tools_context_t *context)
+{
+ if (!context)
+ {
+ return;
+ }
+ tiny_search_index_free(context->search_index);
+ tiny_history_index_free(context->history_index);
+ tiny_memory_index_free(context->memory_index);
+ context->search_index = NULL;
+ context->history_index = NULL;
+ context->memory_index = NULL;
+}
+
+bool
tiny_mcp_tools_context_activate(tiny_mcp_tools_context_t *context)
{
if (!context || active_context)
diff --git a/src/mcp/tools/common/context.h b/src/mcp/tools/common/context.h
index 2a6650a..bd167f2 100644
--- a/src/mcp/tools/common/context.h
+++ b/src/mcp/tools/common/context.h
@@ -3,6 +3,7 @@
#include "domain/search.h"
#include "mcp/tools/common/history.h"
+#include "mcp/tools/common/memory_index.h"
#include "storage/store.h"
typedef struct tiny_mcp_tools_context
@@ -10,6 +11,7 @@ typedef struct tiny_mcp_tools_context
tiny_store_t *store;
tiny_search_index_t *search_index;
tiny_history_index_t *history_index;
+ tiny_memory_index_t *memory_index;
} tiny_mcp_tools_context_t;
tiny_mcp_tools_context_t *tiny_mcp_tools_context_new(tiny_store_t *store);
@@ -19,5 +21,7 @@ void tiny_mcp_tools_context_deactivate(tiny_mcp_tools_context_t *context);
tiny_mcp_tools_context_t *tiny_mcp_tools_context_current(void);
tiny_search_index_t *tiny_mcp_tools_build_search_index(const tiny_store_t *store);
bool tiny_mcp_tools_ensure_history_index(tiny_mcp_tools_context_t *context);
+bool tiny_mcp_tools_ensure_memory_index(tiny_mcp_tools_context_t *context);
+void tiny_mcp_tools_invalidate_indexes(tiny_mcp_tools_context_t *context);
#endif
diff --git a/src/mcp/tools/common/history.c b/src/mcp/tools/common/history.c
index 6254d8c..9f08e84 100644
--- a/src/mcp/tools/common/history.c
+++ b/src/mcp/tools/common/history.c
@@ -284,3 +284,26 @@ tiny_history_index_session(const tiny_history_index_t *index,
}
return index && session_id;
}
+
+size_t
+tiny_history_index_session_count(const tiny_history_index_t *index)
+{
+ return index ? index->count : 0;
+}
+
+bool
+tiny_history_index_session_at(const tiny_history_index_t *index,
+ size_t position,
+ const char **session_id,
+ const size_t **entries,
+ size_t *count)
+{
+ if (!index || position >= index->count || !session_id || !entries || !count)
+ {
+ return false;
+ }
+ *session_id = index->buckets[position].session_id;
+ *entries = index->buckets[position].entries;
+ *count = index->buckets[position].count;
+ return true;
+}
diff --git a/src/mcp/tools/common/history.h b/src/mcp/tools/common/history.h
index ac08d99..d6f5708 100644
--- a/src/mcp/tools/common/history.h
+++ b/src/mcp/tools/common/history.h
@@ -13,5 +13,11 @@ bool tiny_history_index_session(const tiny_history_index_t *index,
const char *session_id,
const size_t **entries,
size_t *count);
+size_t tiny_history_index_session_count(const tiny_history_index_t *index);
+bool tiny_history_index_session_at(const tiny_history_index_t *index,
+ size_t position,
+ const char **session_id,
+ const size_t **entries,
+ size_t *count);
#endif
diff --git a/src/mcp/tools/common/memory_index.c b/src/mcp/tools/common/memory_index.c
new file mode 100644
index 0000000..36e430b
--- /dev/null
+++ b/src/mcp/tools/common/memory_index.c
@@ -0,0 +1,219 @@
+#include "mcp/tools/common/memory_index.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "base/allocator.h"
+
+#define EMPTY_SLOT SIZE_MAX
+
+typedef struct memory_bucket
+{
+ uint64_t hash;
+ char message_id[TINY_MESSAGE_ID_LENGTH + 1];
+ size_t entry_index;
+ tiny_relation_reference_t *incoming;
+ size_t incoming_count;
+ size_t incoming_capacity;
+} memory_bucket_t;
+
+struct tiny_memory_index
+{
+ memory_bucket_t *buckets;
+ size_t count;
+ size_t *slots;
+ size_t slot_capacity;
+};
+
+static uint64_t
+id_hash(const char *value)
+{
+ uint64_t hash = UINT64_C(1469598103934665603);
+ while (*value)
+ {
+ hash ^= (unsigned char)*value++;
+ hash *= UINT64_C(1099511628211);
+ }
+ return hash;
+}
+
+static size_t
+slot_start(uint64_t hash, size_t capacity)
+{
+ return (size_t)(hash ^ (hash >> 32)) & (capacity - 1);
+}
+
+static memory_bucket_t *
+find_bucket(const tiny_memory_index_t *index, const char *message_id)
+{
+ uint64_t hash;
+ size_t slot, bucket;
+ if (!index || !message_id || !index->slot_capacity)
+ {
+ return NULL;
+ }
+ hash = id_hash(message_id);
+ slot = slot_start(hash, index->slot_capacity);
+ while ((bucket = index->slots[slot]) != EMPTY_SLOT)
+ {
+ if (bucket >= index->count)
+ {
+ return NULL;
+ }
+ if (index->buckets[bucket].hash == hash
+ && strcmp(index->buckets[bucket].message_id, message_id) == 0)
+ {
+ return &index->buckets[bucket];
+ }
+ slot = (slot + 1) & (index->slot_capacity - 1);
+ }
+ return NULL;
+}
+
+static bool
+add_incoming(memory_bucket_t *bucket, size_t source, tiny_relation_type_t type)
+{
+ tiny_relation_reference_t *resized;
+ size_t capacity;
+ if (bucket->incoming_count == bucket->incoming_capacity)
+ {
+ capacity = bucket->incoming_capacity ? bucket->incoming_capacity * 2 : 4;
+ if (capacity < bucket->incoming_capacity
+ || !(resized = realloc(bucket->incoming, capacity * sizeof(*resized))))
+ {
+ return false;
+ }
+ bucket->incoming = resized;
+ bucket->incoming_capacity = capacity;
+ }
+ bucket->incoming[bucket->incoming_count].source_index = source;
+ bucket->incoming[bucket->incoming_count].type = type;
+ bucket->incoming_count++;
+ return true;
+}
+
+tiny_memory_index_t *
+tiny_memory_index_build(const tiny_store_t *store)
+{
+ tiny_memory_index_t *index;
+ size_t count = tiny_store_entry_count(store), capacity = 64, position;
+ if (!store || count > SIZE_MAX / sizeof(memory_bucket_t))
+ {
+ return NULL;
+ }
+ while (count > (capacity / 10) * 7)
+ {
+ if (capacity > SIZE_MAX / 2)
+ {
+ return NULL;
+ }
+ capacity *= 2;
+ }
+ index = calloc(1, sizeof(*index));
+ if (!index || (count && !(index->buckets = calloc(count, sizeof(*index->buckets))))
+ || !(index->slots = malloc(capacity * sizeof(*index->slots))))
+ {
+ tiny_memory_index_free(index);
+ return NULL;
+ }
+ index->count = count;
+ index->slot_capacity = capacity;
+ for (position = 0; position < capacity; ++position)
+ {
+ index->slots[position] = EMPTY_SLOT;
+ }
+ for (position = 0; position < count; ++position)
+ {
+ const tiny_store_entry_t *entry = tiny_store_entry(store, position);
+ memory_bucket_t *bucket = &index->buckets[position];
+ size_t slot;
+ memcpy(bucket->message_id, entry->message.message_id, sizeof(bucket->message_id));
+ bucket->hash = id_hash(bucket->message_id);
+ bucket->entry_index = position;
+ slot = slot_start(bucket->hash, capacity);
+ while (index->slots[slot] != EMPTY_SLOT)
+ {
+ slot = (slot + 1) & (capacity - 1);
+ }
+ index->slots[slot] = position;
+ }
+ for (position = 0; position < count; ++position)
+ {
+ const tiny_store_entry_t *entry = tiny_store_entry(store, position);
+ size_t relation;
+ if (entry->deleted)
+ {
+ continue;
+ }
+ for (relation = 0; relation < entry->message.metadata.relation_count; ++relation)
+ {
+ const tiny_relation_t *value = &entry->message.metadata.relations[relation];
+ memory_bucket_t *target = find_bucket(index, value->message_id);
+ if (!target || !add_incoming(target, position, value->type))
+ {
+ tiny_memory_index_free(index);
+ return NULL;
+ }
+ }
+ }
+ return index;
+}
+
+void
+tiny_memory_index_free(tiny_memory_index_t *index)
+{
+ size_t position;
+ if (!index)
+ {
+ return;
+ }
+ for (position = 0; position < index->count; ++position)
+ {
+ free(index->buckets[position].incoming);
+ }
+ free(index->buckets);
+ free(index->slots);
+ free(index);
+}
+
+bool
+tiny_memory_index_find(const tiny_memory_index_t *index,
+ const char *message_id,
+ size_t *entry_index)
+{
+ memory_bucket_t *bucket;
+ if (!entry_index)
+ {
+ return false;
+ }
+ *entry_index = 0;
+ bucket = find_bucket(index, message_id);
+ if (bucket)
+ {
+ *entry_index = bucket->entry_index;
+ }
+ return bucket != NULL;
+}
+
+bool
+tiny_memory_index_incoming(const tiny_memory_index_t *index,
+ const char *message_id,
+ const tiny_relation_reference_t **references,
+ size_t *count)
+{
+ memory_bucket_t *bucket;
+ if (!references || !count)
+ {
+ return false;
+ }
+ *references = NULL;
+ *count = 0;
+ bucket = find_bucket(index, message_id);
+ if (bucket)
+ {
+ *references = bucket->incoming;
+ *count = bucket->incoming_count;
+ }
+ return bucket != NULL;
+}
diff --git a/src/mcp/tools/common/memory_index.h b/src/mcp/tools/common/memory_index.h
new file mode 100644
index 0000000..fbe43a7
--- /dev/null
+++ b/src/mcp/tools/common/memory_index.h
@@ -0,0 +1,24 @@
+#ifndef TINY_CONVMEM_MCP_TOOLS_MEMORY_INDEX_H
+#define TINY_CONVMEM_MCP_TOOLS_MEMORY_INDEX_H
+
+#include "storage/store.h"
+
+typedef struct tiny_relation_reference
+{
+ size_t source_index;
+ tiny_relation_type_t type;
+} tiny_relation_reference_t;
+
+typedef struct tiny_memory_index tiny_memory_index_t;
+
+tiny_memory_index_t *tiny_memory_index_build(const tiny_store_t *store);
+void tiny_memory_index_free(tiny_memory_index_t *index);
+bool tiny_memory_index_find(const tiny_memory_index_t *index,
+ const char *message_id,
+ size_t *entry_index);
+bool tiny_memory_index_incoming(const tiny_memory_index_t *index,
+ const char *message_id,
+ const tiny_relation_reference_t **references,
+ size_t *count);
+
+#endif
diff --git a/src/mcp/tools/common/registry.c b/src/mcp/tools/common/registry.c
index 9aa1dff..1bdafc3 100644
--- a/src/mcp/tools/common/registry.c
+++ b/src/mcp/tools/common/registry.c
@@ -6,6 +6,7 @@ bool
tiny_mcp_tools_register(mcpc_server_t *server)
{
return tiny_mcp_register_write_message(server) && tiny_mcp_register_read_messages(server)
- && tiny_mcp_register_search_messages(server) && tiny_mcp_register_delete_message(server)
- && tiny_mcp_register_rebuild_index(server) && tiny_mcp_register_get_system_health(server);
+ && tiny_mcp_register_search_messages(server) && tiny_mcp_register_update_message(server)
+ && tiny_mcp_register_delete_message(server) && tiny_mcp_register_rebuild_index(server)
+ && tiny_mcp_register_get_system_health(server);
}
diff --git a/src/mcp/tools/common/registry.h b/src/mcp/tools/common/registry.h
index bbc68d0..451d300 100644
--- a/src/mcp/tools/common/registry.h
+++ b/src/mcp/tools/common/registry.h
@@ -4,6 +4,7 @@
#include <mcpc/mcpc.h>
bool tiny_mcp_register_write_message(mcpc_server_t *server);
+bool tiny_mcp_register_update_message(mcpc_server_t *server);
bool tiny_mcp_register_read_messages(mcpc_server_t *server);
bool tiny_mcp_register_search_messages(mcpc_server_t *server);
bool tiny_mcp_register_delete_message(mcpc_server_t *server);
diff --git a/src/mcp/tools/delete_message.c b/src/mcp/tools/delete_message.c
index f90bbe3..9f70866 100644
--- a/src/mcp/tools/delete_message.c
+++ b/src/mcp/tools/delete_message.c
@@ -30,6 +30,10 @@ call_delete_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
}
else
{
+ if (status == TINY_STORE_OK)
+ {
+ tiny_mcp_tools_invalidate_indexes(context);
+ }
mcpc_toolcall_result_add_text_printf8(
result,
(const char8_t *)(status == TINY_STORE_OK ? "{\"status\":\"OK\"}"
diff --git a/src/mcp/tools/get_system_health.c b/src/mcp/tools/get_system_health.c
index 5fd3fc8..f84511f 100644
--- a/src/mcp/tools/get_system_health.c
+++ b/src/mcp/tools/get_system_health.c
@@ -19,7 +19,9 @@ call_get_system_health(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
tiny_json_init(&json);
tiny_json_raw(&json, "{\"status\":");
tiny_json_string(&json, health.last_error[0] ? "DEGRADED" : "OK");
- tiny_json_raw(&json, ",\"format_version\":1,\"log_size\":");
+ tiny_json_raw(&json, ",\"format_version\":");
+ tiny_json_u64(&json, health.format_version);
+ tiny_json_raw(&json, ",\"log_size\":");
tiny_json_u64(&json, health.log_size);
tiny_json_raw(&json, ",\"snapshot_size\":");
tiny_json_u64(&json, health.snapshot_size);
@@ -29,6 +31,21 @@ call_get_system_health(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
tiny_json_u64(&json, health.deleted_messages);
tiny_json_raw(&json, ",\"last_sequence\":");
tiny_json_u64(&json, health.last_sequence);
+ tiny_json_raw(&json, ",\"update_records\":");
+ tiny_json_u64(&json, health.update_records);
+ tiny_json_raw(&json, ",\"dead_bytes\":");
+ tiny_json_u64(&json, health.dead_bytes);
+ tiny_json_raw(&json, ",\"io\":{\"write_bytes\":");
+ tiny_json_u64(&json, health.write_bytes);
+ tiny_json_raw(&json, ",\"write_calls\":");
+ tiny_json_u64(&json, health.write_calls);
+ tiny_json_raw(&json, ",\"flush_calls\":");
+ tiny_json_u64(&json, health.flush_calls);
+ tiny_json_raw(&json, ",\"truncate_calls\":");
+ tiny_json_u64(&json, health.truncate_calls);
+ tiny_json_raw(&json, ",\"replace_calls\":");
+ tiny_json_u64(&json, health.replace_calls);
+ tiny_json_raw(&json, "}");
tiny_json_raw(&json, ",\"recovered_tail\":");
tiny_json_raw(&json, health.recovered_tail ? "true" : "false");
tiny_json_raw(&json, ",\"snapshot_valid\":");
diff --git a/src/mcp/tools/read_messages.c b/src/mcp/tools/read_messages.c
index 44a026f..aa76ce8 100644
--- a/src/mcp/tools/read_messages.c
+++ b/src/mcp/tools/read_messages.c
@@ -4,11 +4,13 @@
#include "mcp/tools/common/context.h"
#include <stdint.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "base/allocator.h"
#include "storage/codec.h"
+#include "mjson.h"
#define READ_DEFAULT_LIMIT 100
#define READ_MAX_LIMIT 1000
@@ -185,10 +187,493 @@ escaped_size(const char *value, size_t length)
return total;
}
+typedef struct context_edge
+{
+ size_t entry_index;
+ tiny_relation_type_t type;
+ size_t depth;
+ bool incoming;
+ int64_t timestamp;
+ char message_id[TINY_MESSAGE_ID_LENGTH + 1];
+} context_edge_t;
+
+static bool
+relation_type_mask(const char *json, uint32_t *mask)
+{
+ int offset = 0, key_offset, key_length, value_offset, value_length, value_type;
+ if (!json)
+ {
+ *mask = UINT32_MAX;
+ return true;
+ }
+ if (mjson_find(json, (int)strlen(json), "$", NULL, NULL) != MJSON_TOK_ARRAY)
+ {
+ return false;
+ }
+ *mask = 0;
+ while ((offset = mjson_next(json,
+ (int)strlen(json),
+ offset,
+ &key_offset,
+ &key_length,
+ &value_offset,
+ &value_length,
+ &value_type))
+ != 0)
+ {
+ char value[24];
+ int decoded;
+ tiny_relation_type_t type;
+ if (value_type != MJSON_TOK_STRING
+ || (decoded = mjson_get_string(
+ json + value_offset, value_length, "$", value, sizeof(value)))
+ <= 0
+ || decoded >= (int)sizeof(value))
+ {
+ return false;
+ }
+ value[decoded] = '\0';
+ if (!tiny_relation_type_parse(value, &type))
+ {
+ return false;
+ }
+ *mask |= UINT32_C(1) << (unsigned)type;
+ }
+ return true;
+}
+
+static int
+compare_context_edges(const void *left, const void *right)
+{
+ const context_edge_t *a = left, *b = right;
+ if (a->depth != b->depth)
+ return a->depth < b->depth ? -1 : 1;
+ if (a->type != b->type)
+ return a->type < b->type ? -1 : 1;
+ if (a->timestamp != b->timestamp)
+ return a->timestamp < b->timestamp ? -1 : 1;
+ return strcmp(a->message_id, b->message_id);
+}
+
+static void
+call_context_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mcpc_ucbr_t *result)
+{
+ char *message_id = tiny_mcp_tool_get_string(tool, "message_id", TINY_MESSAGE_ID_LENGTH);
+ char *types_json = tiny_mcp_tool_get_string(tool, "relation_types_json", 256);
+ int32_t before = 1, after = 1, relation_depth = 1;
+ mcpc_errcode_t before_status
+ = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"before", &before);
+ mcpc_errcode_t after_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"after", &after);
+ mcpc_errcode_t depth_status
+ = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"relation_depth", &relation_depth);
+ const tiny_store_entry_t *target = NULL;
+ const size_t *entries = NULL;
+ const tiny_relation_reference_t *incoming = NULL;
+ size_t session_entry_count = 0, position, target_position = SIZE_MAX, emitted, message_count = 1;
+ size_t incoming_count = 0;
+ size_t target_index, queue[50], queue_depth[50], queue_head = 0, queue_count = 0;
+ context_edge_t edges[49];
+ size_t edge_count = 0;
+ bool *visited = NULL, truncated = false;
+ uint32_t type_mask;
+ tiny_json_buffer_t json;
+ if (!message_id || before < 0 || before > 10 || after < 0 || after > 10
+ || relation_depth < 0 || relation_depth > 2
+ || (before_status != MCPC_EC_0 && before_status != MCPC_EC_ANYDATA_NOTFOUND)
+ || (after_status != MCPC_EC_0 && after_status != MCPC_EC_ANYDATA_NOTFOUND)
+ || (depth_status != MCPC_EC_0 && depth_status != MCPC_EC_ANYDATA_NOTFOUND)
+ || !relation_type_mask(types_json, &type_mask) || !context
+ || !tiny_mcp_tools_ensure_memory_index(context)
+ || !tiny_memory_index_find(context->memory_index, message_id, &target_index))
+ {
+ tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid context view arguments");
+ free(message_id);
+ free(types_json);
+ return;
+ }
+ target = tiny_store_entry(context->store, target_index);
+ if (!target || target->deleted || !tiny_mcp_tools_ensure_history_index(context)
+ || !tiny_history_index_session(
+ context->history_index, target->message.session_id, &entries, &session_entry_count))
+ {
+ tiny_mcp_tool_add_error(result, "NOT_FOUND", "message context was not found");
+ free(message_id);
+ free(types_json);
+ return;
+ }
+ for (position = 0; position < session_entry_count; ++position)
+ {
+ if (strcmp(tiny_store_entry(context->store, entries[position])->message.message_id,
+ message_id)
+ == 0)
+ {
+ target_position = position;
+ break;
+ }
+ }
+ if (target_position == SIZE_MAX
+ || !(visited = calloc(tiny_store_entry_count(context->store), sizeof(bool))))
+ {
+ tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "context index allocation failed");
+ free(message_id);
+ free(types_json);
+ return;
+ }
+ visited[target_index] = true;
+ queue[queue_count] = target_index;
+ queue_depth[queue_count++] = 0;
+ while (queue_head < queue_count && edge_count < 49)
+ {
+ size_t current_index = queue[queue_head];
+ size_t depth = queue_depth[queue_head++];
+ const tiny_store_entry_t *current = tiny_store_entry(context->store, current_index);
+ size_t relation;
+ if (depth >= (size_t)relation_depth)
+ {
+ continue;
+ }
+ for (relation = 0; relation < current->message.metadata.relation_count; ++relation)
+ {
+ const tiny_relation_t *value = &current->message.metadata.relations[relation];
+ size_t related_index;
+ const tiny_store_entry_t *related;
+ if (!(type_mask & (UINT32_C(1) << (unsigned)value->type))
+ || !tiny_memory_index_find(context->memory_index, value->message_id, &related_index)
+ || visited[related_index])
+ {
+ continue;
+ }
+ related = tiny_store_entry(context->store, related_index);
+ visited[related_index] = true;
+ edges[edge_count].entry_index = related_index;
+ edges[edge_count].type = value->type;
+ edges[edge_count].depth = depth + 1;
+ edges[edge_count].incoming = false;
+ edges[edge_count].timestamp = related->message.timestamp_ms;
+ memcpy(edges[edge_count].message_id,
+ related->message.message_id,
+ sizeof(edges[edge_count].message_id));
+ edge_count++;
+ if (!related->deleted && queue_count < 50)
+ {
+ queue[queue_count] = related_index;
+ queue_depth[queue_count++] = depth + 1;
+ }
+ if (edge_count == 49)
+ {
+ truncated = true;
+ break;
+ }
+ }
+ if (edge_count == 49)
+ {
+ break;
+ }
+ if (!tiny_memory_index_incoming(
+ context->memory_index,
+ current->message.message_id,
+ &incoming,
+ &incoming_count))
+ {
+ continue;
+ }
+ for (relation = 0; relation < incoming_count && edge_count < 49; ++relation)
+ {
+ const tiny_store_entry_t *related
+ = tiny_store_entry(context->store, incoming[relation].source_index);
+ if (!(type_mask & (UINT32_C(1) << (unsigned)incoming[relation].type))
+ || visited[incoming[relation].source_index])
+ {
+ continue;
+ }
+ visited[incoming[relation].source_index] = true;
+ edges[edge_count].entry_index = incoming[relation].source_index;
+ edges[edge_count].type = incoming[relation].type;
+ edges[edge_count].depth = depth + 1;
+ edges[edge_count].incoming = true;
+ edges[edge_count].timestamp = related->message.timestamp_ms;
+ memcpy(edges[edge_count].message_id,
+ related->message.message_id,
+ sizeof(edges[edge_count].message_id));
+ edge_count++;
+ if (!related->deleted && queue_count < 50)
+ {
+ queue[queue_count] = incoming[relation].source_index;
+ queue_depth[queue_count++] = depth + 1;
+ }
+ }
+ }
+ qsort(edges, edge_count, sizeof(*edges), compare_context_edges);
+ tiny_json_init(&json);
+ tiny_json_raw(&json, "{\"message\":");
+ tiny_mcp_tool_append_message(&json, &target->message);
+ tiny_json_raw(&json, ",\"chronological_context\":{\"before\":[");
+ emitted = 0;
+ position = target_position > (size_t)before ? target_position - (size_t)before : 0;
+ for (; position < target_position; ++position)
+ {
+ const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[position]);
+ if (entry && !entry->deleted)
+ {
+ tiny_json_buffer_t item;
+ tiny_json_init(&item);
+ tiny_mcp_tool_append_message(&item, &entry->message);
+ if (message_count >= 50 || item.failed
+ || json.length + item.length + 256 > READ_RESPONSE_BUDGET)
+ {
+ truncated = true;
+ tiny_json_free(&item);
+ break;
+ }
+ if (emitted++)
+ {
+ tiny_json_raw(&json, ",");
+ }
+ tiny_json_raw_n(&json, item.data, item.length);
+ tiny_json_free(&item);
+ message_count++;
+ }
+ }
+ tiny_json_raw(&json, "],\"after\":[");
+ emitted = 0;
+ for (position = target_position + 1;
+ position < session_entry_count && emitted < (size_t)after;
+ ++position)
+ {
+ const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[position]);
+ if (entry && !entry->deleted)
+ {
+ tiny_json_buffer_t item;
+ tiny_json_init(&item);
+ tiny_mcp_tool_append_message(&item, &entry->message);
+ if (message_count >= 50 || item.failed
+ || json.length + item.length + 256 > READ_RESPONSE_BUDGET)
+ {
+ truncated = true;
+ tiny_json_free(&item);
+ break;
+ }
+ if (emitted++)
+ {
+ tiny_json_raw(&json, ",");
+ }
+ tiny_json_raw_n(&json, item.data, item.length);
+ tiny_json_free(&item);
+ message_count++;
+ }
+ }
+ tiny_json_raw(&json, "]},\"relations\":[");
+ emitted = 0;
+ for (position = 0; position < edge_count; ++position)
+ {
+ const context_edge_t *edge = &edges[position];
+ const tiny_store_entry_t *related = tiny_store_entry(context->store, edge->entry_index);
+ tiny_json_buffer_t item;
+ tiny_json_init(&item);
+ tiny_json_raw(&item, "{\"type\":");
+ tiny_json_string(&item, tiny_relation_type_name(edge->type));
+ tiny_json_raw(&item, edge->incoming ? ",\"direction\":\"incoming\"" : ",\"direction\":\"outgoing\"");
+ tiny_json_raw(&item, ",\"depth\":");
+ tiny_json_u64(&item, edge->depth);
+ if (related && !related->deleted)
+ {
+ tiny_json_raw(&item, ",\"message\":");
+ tiny_mcp_tool_append_message(&item, &related->message);
+ }
+ else
+ {
+ tiny_json_raw(&item, ",\"message_id\":");
+ tiny_json_string(&item, edge->message_id);
+ tiny_json_raw(&item, ",\"status\":\"deleted\"");
+ }
+ tiny_json_raw(&item, "}");
+ if (message_count >= 50 || item.failed
+ || json.length + item.length + 256 > READ_RESPONSE_BUDGET)
+ {
+ truncated = true;
+ tiny_json_free(&item);
+ break;
+ }
+ if (emitted++)
+ {
+ tiny_json_raw(&json, ",");
+ }
+ tiny_json_raw_n(&json, item.data, item.length);
+ tiny_json_free(&item);
+ if (related && !related->deleted)
+ {
+ message_count++;
+ }
+ }
+ tiny_json_raw(&json, "],\"session\":{\"session_id\":");
+ tiny_json_string(&json, target->message.session_id);
+ tiny_json_raw(&json, ",\"message_count\":");
+ tiny_json_u64(&json, session_entry_count);
+ tiny_json_raw(&json, "},\"truncated\":");
+ tiny_json_raw(&json, truncated ? "true" : "false");
+ tiny_json_raw(&json, "}");
+ tiny_mcp_tool_add_json(result, &json);
+ tiny_json_free(&json);
+ free(visited);
+ free(message_id);
+ free(types_json);
+}
+
+static size_t
+simple_cursor(const char *cursor, bool *valid)
+{
+ char *end = NULL;
+ unsigned long long value;
+ if (!cursor)
+ {
+ *valid = true;
+ return 0;
+ }
+ value = strtoull(cursor, &end, 10);
+ *valid = cursor[0] && end && !*end && value <= SIZE_MAX;
+ return *valid ? (size_t)value : 0;
+}
+
+static void
+call_sessions_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mcpc_ucbr_t *result)
+{
+ char *cursor = tiny_mcp_tool_get_string(tool, "cursor", CURSOR_TEXT_MAX);
+ int32_t limit = READ_DEFAULT_LIMIT;
+ bool valid;
+ size_t start = simple_cursor(cursor, &valid), index, unique = 0, emitted = 0;
+ tiny_json_buffer_t json;
+ mcpc_errcode_t limit_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &limit);
+ if (!context || !valid || limit < 1 || limit > READ_MAX_LIMIT
+ || (limit_status != MCPC_EC_0 && limit_status != MCPC_EC_ANYDATA_NOTFOUND))
+ {
+ tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid sessions view arguments");
+ free(cursor);
+ return;
+ }
+ tiny_json_init(&json);
+ tiny_json_raw(&json, "{\"sessions\":[");
+ for (index = 0; index < tiny_store_entry_count(context->store) && emitted < (size_t)limit;
+ ++index)
+ {
+ const tiny_store_entry_t *entry = tiny_store_entry(context->store, index);
+ size_t prior, scan, count = 0;
+ int64_t started = INT64_MAX, ended = 0;
+ if (!entry || entry->deleted)
+ {
+ continue;
+ }
+ for (prior = 0; prior < index; ++prior)
+ {
+ const tiny_store_entry_t *candidate = tiny_store_entry(context->store, prior);
+ if (candidate && !candidate->deleted
+ && strcmp(candidate->message.session_id, entry->message.session_id) == 0)
+ {
+ break;
+ }
+ }
+ if (prior != index || unique++ < start)
+ {
+ continue;
+ }
+ for (scan = index; scan < tiny_store_entry_count(context->store); ++scan)
+ {
+ const tiny_store_entry_t *candidate = tiny_store_entry(context->store, scan);
+ if (candidate && !candidate->deleted
+ && strcmp(candidate->message.session_id, entry->message.session_id) == 0)
+ {
+ count++;
+ started = candidate->message.timestamp_ms < started ? candidate->message.timestamp_ms
+ : started;
+ ended = candidate->message.timestamp_ms > ended ? candidate->message.timestamp_ms
+ : ended;
+ }
+ }
+ if (emitted++)
+ {
+ tiny_json_raw(&json, ",");
+ }
+ tiny_json_raw(&json, "{\"session_id\":");
+ tiny_json_string(&json, entry->message.session_id);
+ tiny_json_raw(&json, ",\"message_count\":");
+ tiny_json_u64(&json, count);
+ tiny_json_raw(&json, ",\"started_at\":");
+ tiny_json_i64(&json, started);
+ tiny_json_raw(&json, ",\"last_message_at\":");
+ tiny_json_i64(&json, ended);
+ tiny_json_raw(&json, "}");
+ }
+ tiny_json_raw(&json, "]");
+ if (emitted == (size_t)limit)
+ {
+ char next[32];
+ snprintf(next, sizeof(next), "%zu", start + emitted);
+ tiny_json_raw(&json, ",\"nextCursor\":");
+ tiny_json_string(&json, next);
+ }
+ tiny_json_raw(&json, "}");
+ tiny_mcp_tool_add_json(result, &json);
+ tiny_json_free(&json);
+ free(cursor);
+}
+
+static void
+call_export_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mcpc_ucbr_t *result)
+{
+ char *session = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX);
+ char *cursor = tiny_mcp_tool_get_string(tool, "cursor", CURSOR_TEXT_MAX);
+ int32_t limit = 500;
+ bool valid;
+ size_t index = simple_cursor(cursor, &valid), emitted = 0;
+ tiny_json_buffer_t json, lines;
+ mcpc_errcode_t limit_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &limit);
+ if (!context || !valid || limit < 1 || limit > 1000
+ || (limit_status != MCPC_EC_0 && limit_status != MCPC_EC_ANYDATA_NOTFOUND))
+ {
+ tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid export view arguments");
+ free(session);
+ free(cursor);
+ return;
+ }
+ tiny_json_init(&lines);
+ if (index == 0)
+ {
+ tiny_json_raw(&lines, "{\"type\":\"tiny-convmem-manifest\",\"format_version\":2}\n");
+ }
+ for (; index < tiny_store_entry_count(context->store) && emitted < (size_t)limit; ++index)
+ {
+ const tiny_store_entry_t *entry = tiny_store_entry(context->store, index);
+ if (!entry || entry->deleted
+ || (session && session[0] && strcmp(session, entry->message.session_id) != 0))
+ {
+ continue;
+ }
+ tiny_mcp_tool_append_message(&lines, &entry->message);
+ tiny_json_raw(&lines, "\n");
+ emitted++;
+ }
+ tiny_json_init(&json);
+ tiny_json_raw(&json, "{\"jsonl\":");
+ tiny_json_string(&json, lines.data ? lines.data : "");
+ if (index < tiny_store_entry_count(context->store))
+ {
+ char next[32];
+ snprintf(next, sizeof(next), "%zu", index);
+ tiny_json_raw(&json, ",\"nextCursor\":");
+ tiny_json_string(&json, next);
+ }
+ tiny_json_raw(&json, "}");
+ tiny_mcp_tool_add_json(result, &json);
+ tiny_json_free(&json);
+ tiny_json_free(&lines);
+ free(session);
+ free(cursor);
+}
+
static void
call_read_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
{
tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current();
+ char *view = tiny_mcp_tool_get_string(tool, "view", 16);
char *session = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX);
char *cursor_text = tiny_mcp_tool_get_string(tool, "cursor", CURSOR_TEXT_MAX);
int32_t requested_limit = READ_DEFAULT_LIMIT;
@@ -201,6 +686,40 @@ call_read_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
const tiny_message_t *last = NULL;
tiny_json_buffer_t json;
+ if (view && strcmp(view, "context") == 0)
+ {
+ call_context_view(context, tool, result);
+ free(view);
+ free(session);
+ free(cursor_text);
+ return;
+ }
+ if (view && strcmp(view, "sessions") == 0)
+ {
+ call_sessions_view(context, tool, result);
+ free(view);
+ free(session);
+ free(cursor_text);
+ return;
+ }
+ if (view && strcmp(view, "export") == 0)
+ {
+ call_export_view(context, tool, result);
+ free(view);
+ free(session);
+ free(cursor_text);
+ return;
+ }
+ if (view && strcmp(view, "history") != 0)
+ {
+ tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "unsupported read_messages view");
+ free(view);
+ free(session);
+ free(cursor_text);
+ return;
+ }
+ free(view);
+
if (!context || !session || !session[0]
|| (limit_status != MCPC_EC_0 && limit_status != MCPC_EC_ANYDATA_NOTFOUND)
|| requested_limit < 1 || requested_limit > READ_MAX_LIMIT
@@ -324,11 +843,33 @@ tiny_mcp_register_read_messages(mcpc_server_t *server)
"Returns a paginated history for reconstructing that conversation.",
call_read_messages);
return tool
+ && tiny_mcp_tool_add_property(
+ tool,
+ "view",
+ "Read view: history (default), context, sessions, or export.",
+ MCPC_U8STR,
+ false)
&& tiny_mcp_tool_add_property(tool,
"session_id",
"Conversation session whose chronological history to read.",
MCPC_U8STR,
- true)
+ false)
+ && tiny_mcp_tool_add_property(
+ tool, "message_id", "Message ULID required by the context view.", MCPC_U8STR, false)
+ && tiny_mcp_tool_add_property(
+ tool, "before", "Context messages before the target, from 0 to 10.", MCPC_I32, false)
+ && tiny_mcp_tool_add_property(
+ tool, "after", "Context messages after the target, from 0 to 10.", MCPC_I32, false)
+ && tiny_mcp_tool_add_property(tool,
+ "relation_depth",
+ "Relation traversal depth from 0 to 2; defaults to 1.",
+ MCPC_I32,
+ false)
+ && tiny_mcp_tool_add_property(tool,
+ "relation_types_json",
+ "Optional JSON array string of relation types to follow.",
+ MCPC_U8STR,
+ false)
&& tiny_mcp_tool_add_property(
tool,
"limit",
diff --git a/src/mcp/tools/rebuild_index.c b/src/mcp/tools/rebuild_index.c
index e6dbf69..d9a5e87 100644
--- a/src/mcp/tools/rebuild_index.c
+++ b/src/mcp/tools/rebuild_index.c
@@ -10,8 +10,6 @@ call_rebuild_index(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
{
tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current();
tiny_rebuild_report_t report;
- tiny_search_index_t *rebuilt;
- tiny_history_index_t *history;
tiny_store_status_t status;
tiny_json_buffer_t json;
(void)tool;
@@ -26,22 +24,7 @@ call_rebuild_index(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
tiny_mcp_tool_add_error(result, tiny_store_status_name(status), "index rebuild failed");
return;
}
- tiny_history_index_free(context->history_index);
- context->history_index = NULL;
- rebuilt = tiny_mcp_tools_build_search_index(context->store);
- history = tiny_history_index_build(context->store);
- if (!rebuilt || !history)
- {
- tiny_search_index_free(rebuilt);
- tiny_history_index_free(history);
- tiny_search_index_free(context->search_index);
- context->search_index = NULL;
- tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "search index allocation failed");
- return;
- }
- tiny_search_index_free(context->search_index);
- context->search_index = rebuilt;
- context->history_index = history;
+ tiny_mcp_tools_invalidate_indexes(context);
tiny_json_init(&json);
tiny_json_raw(&json, "{\"status\":\"OK\",\"messages_indexed\":");
tiny_json_u64(&json, report.messages_indexed);
diff --git a/src/mcp/tools/search_messages.c b/src/mcp/tools/search_messages.c
index 44db1ef..4b7f870 100644
--- a/src/mcp/tools/search_messages.c
+++ b/src/mcp/tools/search_messages.c
@@ -13,8 +13,31 @@ typedef struct result_item
{
const tiny_store_entry_t *entry;
double score;
+ double content_score;
+ double summary_score;
+ double tag_score;
} result_item_t;
+static double
+score_component(const char *query, const tiny_message_t *message, int component)
+{
+ tiny_message_t candidate = *message;
+ candidate.metadata.tags = NULL;
+ candidate.metadata.tag_count = 0;
+ candidate.metadata.summary = NULL;
+ if (component == 1)
+ {
+ candidate.content = message->metadata.summary ? message->metadata.summary : "";
+ }
+ else if (component == 2)
+ {
+ candidate.content = "";
+ candidate.metadata.tags = message->metadata.tags;
+ candidate.metadata.tag_count = message->metadata.tag_count;
+ }
+ return tiny_search_score(query, &candidate);
+}
+
static int
compare_results(const void *left, const void *right)
{
@@ -28,6 +51,22 @@ compare_results(const void *left, const void *right)
{
return 1;
}
+ if (a->entry->message.metadata.importance > b->entry->message.metadata.importance)
+ {
+ return -1;
+ }
+ if (a->entry->message.metadata.importance < b->entry->message.metadata.importance)
+ {
+ return 1;
+ }
+ if (a->entry->message.updated_at_ms > b->entry->message.updated_at_ms)
+ {
+ return -1;
+ }
+ if (a->entry->message.updated_at_ms < b->entry->message.updated_at_ms)
+ {
+ return 1;
+ }
if (a->entry->message.timestamp_ms > b->entry->message.timestamp_ms)
{
return -1;
@@ -45,15 +84,27 @@ call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current();
char *query = tiny_mcp_tool_get_string(tool, "query", TINY_QUERY_MAX);
char *session = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX);
+ char *kind_text = tiny_mcp_tool_get_string(tool, "memory_kind", 24);
+ char *tier_text = tiny_mcp_tool_get_string(tool, "memory_tier", 16);
+ char *tag = tiny_mcp_tool_get_string(tool, "tag", TINY_TAG_MAX);
int32_t requested_limit = 20;
+ int32_t minimum_importance = 0;
mcpc_errcode_t limit_status
= mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &requested_limit);
+ mcpc_errcode_t importance_status
+ = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"min_importance", &minimum_importance);
+ tiny_memory_kind_t kind = TINY_KIND_CONVERSATION;
+ tiny_memory_tier_t tier = TINY_TIER_SHORT;
size_t limit, index, count = 0, candidate_count = 0, *candidates = NULL;
result_item_t *items = NULL;
tiny_json_buffer_t json;
bool valid = context && query && query[0]
&& (limit_status == MCPC_EC_0 || limit_status == MCPC_EC_ANYDATA_NOTFOUND)
&& requested_limit >= 1 && requested_limit <= 100;
+ valid = valid && minimum_importance >= 0 && minimum_importance <= 100
+ && (importance_status == MCPC_EC_0 || importance_status == MCPC_EC_ANYDATA_NOTFOUND)
+ && (!kind_text || tiny_kind_parse(kind_text, &kind))
+ && (!tier_text || tiny_tier_parse(tier_text, &tier));
if (!valid)
{
tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "query or limit is invalid");
@@ -90,11 +141,37 @@ call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
{
continue;
}
+ if ((kind_text && entry->message.metadata.memory_kind != kind)
+ || (tier_text && entry->message.metadata.memory_tier != tier)
+ || entry->message.metadata.importance < minimum_importance)
+ {
+ continue;
+ }
+ if (tag)
+ {
+ size_t tag_index;
+ for (tag_index = 0; tag_index < entry->message.metadata.tag_count;
+ ++tag_index)
+ {
+ if (strcmp(tag, entry->message.metadata.tags[tag_index]) == 0)
+ {
+ break;
+ }
+ }
+ if (tag_index == entry->message.metadata.tag_count)
+ {
+ continue;
+ }
+ }
score = tiny_search_score(query, &entry->message);
if (score > 0)
{
items[count].entry = entry;
- items[count++].score = score;
+ items[count].score = score;
+ items[count].content_score = score_component(query, &entry->message, 0);
+ items[count].summary_score = score_component(query, &entry->message, 1);
+ items[count].tag_score = score_component(query, &entry->message, 2);
+ count++;
}
}
qsort(items, count, sizeof(*items), compare_results);
@@ -110,7 +187,47 @@ call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
{
tiny_json_raw(&json, ",");
}
- tiny_mcp_tool_append_message(&json, &items[index].entry->message);
+ {
+ tiny_json_buffer_t item;
+ tiny_json_init(&item);
+ tiny_mcp_tool_append_message(&item, &items[index].entry->message);
+ if (item.length > 0)
+ {
+ item.length--;
+ item.data[item.length] = '\0';
+ }
+ tiny_json_raw(&item, ",\"score\":");
+ tiny_json_number(&item, items[index].score);
+ tiny_json_raw(&item, ",\"score_breakdown\":{\"content\":");
+ tiny_json_number(&item, items[index].content_score);
+ tiny_json_raw(&item, ",\"summary\":");
+ tiny_json_number(&item, items[index].summary_score);
+ tiny_json_raw(&item, ",\"tags\":");
+ tiny_json_number(&item, items[index].tag_score);
+ tiny_json_raw(&item, ",\"importance\":0},\"matched_fields\":[");
+ {
+ bool matched = false;
+ if (items[index].content_score > 0)
+ {
+ tiny_json_raw(&item, "\"content\"");
+ matched = true;
+ }
+ if (items[index].summary_score > 0)
+ {
+ tiny_json_raw(&item, matched ? ",\"summary\"" : "\"summary\"");
+ matched = true;
+ }
+ if (items[index].tag_score > 0)
+ {
+ tiny_json_raw(&item, matched ? ",\"tags\"" : "\"tags\"");
+ }
+ }
+ tiny_json_raw(&item, "],\"relation_count\":");
+ tiny_json_u64(&item, items[index].entry->message.metadata.relation_count);
+ tiny_json_raw(&item, "}");
+ tiny_json_raw_n(&json, item.data, item.length);
+ tiny_json_free(&item);
+ }
}
tiny_json_raw(&json, "]}");
tiny_mcp_tool_add_json(result, &json);
@@ -122,6 +239,9 @@ call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
free(candidates);
free(query);
free(session);
+ free(kind_text);
+ free(tier_text);
+ free(tag);
}
bool
@@ -146,6 +266,16 @@ tiny_mcp_register_search_messages(mcpc_server_t *server)
MCPC_U8STR,
false)
&& tiny_mcp_tool_add_property(
+ tool, "memory_kind", "Optional exact memory kind filter.", MCPC_U8STR, false)
+ && tiny_mcp_tool_add_property(
+ tool, "memory_tier", "Optional exact memory tier filter.", MCPC_U8STR, false)
+ && tiny_mcp_tool_add_property(tool, "tag", "Optional exact tag filter.", MCPC_U8STR, false)
+ && tiny_mcp_tool_add_property(tool,
+ "min_importance",
+ "Minimum importance from 0 to 100; defaults to 0.",
+ MCPC_I32,
+ false)
+ && tiny_mcp_tool_add_property(
tool,
"limit",
"Maximum ranked matches to return, from 1 to 100; defaults to 20.",
diff --git a/src/mcp/tools/update_message.c b/src/mcp/tools/update_message.c
new file mode 100644
index 0000000..aca1bed
--- /dev/null
+++ b/src/mcp/tools/update_message.c
@@ -0,0 +1,97 @@
+#include "mcp/tools/common/registry.h"
+
+#include "mcp/tools/common/common.h"
+#include "mcp/tools/common/context.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+static void
+call_update_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
+{
+ tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current();
+ char *message_id = tiny_mcp_tool_get_string(tool, "message_id", TINY_MESSAGE_ID_LENGTH);
+ char *content = tiny_mcp_tool_get_string(tool, "content", TINY_CONTENT_MAX);
+ char *metadata_json = tiny_mcp_tool_get_string(tool, "metadata_json", TINY_CONTENT_MAX);
+ int32_t expected = 0;
+ mcpc_errcode_t expected_status
+ = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"expected_revision", &expected);
+ tiny_message_t parsed, updated;
+ tiny_message_patch_t patch;
+ tiny_store_status_t status;
+ tiny_json_buffer_t json;
+ bool changed = false;
+ tiny_metadata_mask_t metadata_mask = 0;
+ tiny_message_init(&parsed);
+ tiny_message_init(&updated);
+ patch.content = content;
+ patch.metadata = metadata_json ? &parsed.metadata : NULL;
+ patch.metadata_mask = 0;
+ patch.expected_revision = (uint64_t)expected;
+ patch.has_expected_revision = expected_status == MCPC_EC_0;
+ if (!context || !message_id || (!content && !metadata_json)
+ || (metadata_json && !tiny_mcp_parse_metadata(metadata_json, &parsed, &metadata_mask))
+ || (expected_status != MCPC_EC_0 && expected_status != MCPC_EC_ANYDATA_NOTFOUND)
+ || (patch.has_expected_revision && expected < 1))
+ {
+ tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid update_message arguments");
+ }
+ else
+ {
+ patch.metadata_mask = metadata_mask;
+ status = tiny_store_update(context->store, message_id, &patch, &updated, &changed);
+ if (status != TINY_STORE_OK)
+ {
+ tiny_mcp_tool_add_error(
+ result, tiny_store_status_name(status), "message was not updated");
+ }
+ else
+ {
+ if (changed)
+ {
+ tiny_mcp_tools_invalidate_indexes(context);
+ }
+ tiny_json_init(&json);
+ tiny_json_raw(&json, "{\"status\":\"OK\",\"changed\":");
+ tiny_json_raw(&json, changed ? "true" : "false");
+ tiny_json_raw(&json, ",\"message\":");
+ tiny_mcp_tool_append_message(&json, &updated);
+ tiny_json_raw(&json, "}");
+ tiny_mcp_tool_add_json(result, &json);
+ tiny_json_free(&json);
+ }
+ }
+ free(message_id);
+ free(content);
+ free(metadata_json);
+ tiny_message_free(&parsed);
+ tiny_message_free(&updated);
+}
+
+bool
+tiny_mcp_register_update_message(mcpc_server_t *server)
+{
+ mcpc_tool_t *tool = tiny_mcp_tool_new("update_message",
+ "Update message content or metadata while preserving "
+ "identity, session, role, and creation time.",
+ call_update_message);
+ return tool
+ && tiny_mcp_tool_add_property(
+ tool, "message_id", "ULID of the live message to update.", MCPC_U8STR, true)
+ && tiny_mcp_tool_add_property(tool,
+ "content",
+ "Replacement UTF-8 content; omit to retain content.",
+ MCPC_U8STR,
+ false)
+ && tiny_mcp_tool_add_property(tool,
+ "metadata_json",
+ "Replacement metadata JSON; omit to retain metadata.",
+ MCPC_U8STR,
+ false)
+ && tiny_mcp_tool_add_property(tool,
+ "expected_revision",
+ "Optional positive revision for optimistic concurrency.",
+ MCPC_I32,
+ false)
+ && mcpc_server_add_tool(server, tool) == MCPC_EC_0;
+}
diff --git a/src/mcp/tools/write_message.c b/src/mcp/tools/write_message.c
index 7df0058..3d7cb29 100644
--- a/src/mcp/tools/write_message.c
+++ b/src/mcp/tools/write_message.c
@@ -34,7 +34,9 @@ metadata_has_only_known_fields(const char *json, int length)
}
key[decoded] = '\0';
if (strcmp(key, "tool_call_id") != 0 && strcmp(key, "tags") != 0
- && strcmp(key, "memory_tier") != 0)
+ && strcmp(key, "memory_tier") != 0 && strcmp(key, "memory_kind") != 0
+ && strcmp(key, "importance") != 0 && strcmp(key, "summary") != 0
+ && strcmp(key, "relations") != 0 && strcmp(key, "source_message_ids") != 0)
{
return false;
}
@@ -42,14 +44,20 @@ metadata_has_only_known_fields(const char *json, int length)
return true;
}
-static bool
-parse_metadata(const char *json, tiny_message_t *message)
+bool
+tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata_mask_t *present)
{
- const char *tool_call = NULL, *tier = NULL, *tags = NULL;
- int tool_call_len = 0, tier_len = 0, tags_len = 0;
+ const char *tool_call = NULL, *tier = NULL, *tags = NULL, *kind = NULL, *summary = NULL;
+ const char *relations = NULL, *sources = NULL;
+ int tool_call_len = 0, tier_len = 0, tags_len = 0, kind_len = 0, summary_len = 0;
+ int relations_len = 0, sources_len = 0;
int offset = 0, key_offset, key_length, value_offset, value_length, value_type;
int length = (int)strlen(json);
int tool_call_type, tier_type, tags_type;
+ int kind_type, summary_type, importance_type;
+ int relations_type, sources_type;
+ double importance = 50;
+ tiny_metadata_mask_t fields = 0;
if (strcmp(json, "{}") == 0)
{
return true;
@@ -62,12 +70,176 @@ parse_metadata(const char *json, tiny_message_t *message)
tool_call_type = mjson_find(json, length, "$.tool_call_id", &tool_call, &tool_call_len);
tier_type = mjson_find(json, length, "$.memory_tier", &tier, &tier_len);
tags_type = mjson_find(json, length, "$.tags", &tags, &tags_len);
+ kind_type = mjson_find(json, length, "$.memory_kind", &kind, &kind_len);
+ summary_type = mjson_find(json, length, "$.summary", &summary, &summary_len);
+ importance_type = mjson_find(json, length, "$.importance", NULL, NULL);
+ relations_type = mjson_find(json, length, "$.relations", &relations, &relations_len);
+ sources_type = mjson_find(json, length, "$.source_message_ids", &sources, &sources_len);
if ((tool_call_type != MJSON_TOK_INVALID && tool_call_type != MJSON_TOK_STRING)
|| (tier_type != MJSON_TOK_INVALID && tier_type != MJSON_TOK_STRING)
- || (tags_type != MJSON_TOK_INVALID && tags_type != MJSON_TOK_ARRAY))
+ || (tags_type != MJSON_TOK_INVALID && tags_type != MJSON_TOK_ARRAY)
+ || (kind_type != MJSON_TOK_INVALID && kind_type != MJSON_TOK_STRING)
+ || (summary_type != MJSON_TOK_INVALID && summary_type != MJSON_TOK_STRING)
+ || (importance_type != MJSON_TOK_INVALID && importance_type != MJSON_TOK_NUMBER)
+ || (relations_type != MJSON_TOK_INVALID && relations_type != MJSON_TOK_ARRAY)
+ || (sources_type != MJSON_TOK_INVALID && sources_type != MJSON_TOK_ARRAY))
{
return false;
}
+ fields |= tool_call_type != MJSON_TOK_INVALID ? TINY_METADATA_TOOL_CALL_ID : 0;
+ fields |= tags_type != MJSON_TOK_INVALID ? TINY_METADATA_TAGS : 0;
+ fields |= tier_type != MJSON_TOK_INVALID ? TINY_METADATA_TIER : 0;
+ fields |= kind_type != MJSON_TOK_INVALID ? TINY_METADATA_KIND : 0;
+ fields |= importance_type != MJSON_TOK_INVALID ? TINY_METADATA_IMPORTANCE : 0;
+ fields |= summary_type != MJSON_TOK_INVALID ? TINY_METADATA_SUMMARY : 0;
+ fields |= relations_type != MJSON_TOK_INVALID ? TINY_METADATA_RELATIONS : 0;
+ fields |= sources_type != MJSON_TOK_INVALID ? TINY_METADATA_SOURCES : 0;
+ if (relations_type == MJSON_TOK_ARRAY)
+ {
+ offset = 0;
+ while ((offset = mjson_next(relations,
+ relations_len,
+ offset,
+ &key_offset,
+ &key_length,
+ &value_offset,
+ &value_length,
+ &value_type))
+ != 0)
+ {
+ tiny_relation_t relation;
+ char type[24];
+ int decoded_type, decoded_id;
+ tiny_relation_t *resized;
+ if (value_type != MJSON_TOK_OBJECT
+ || message->metadata.relation_count >= TINY_RELATION_COUNT_MAX)
+ {
+ return false;
+ }
+ decoded_type = mjson_get_string(
+ relations + value_offset, value_length, "$.type", type, sizeof(type));
+ decoded_id = mjson_get_string(relations + value_offset,
+ value_length,
+ "$.message_id",
+ relation.message_id,
+ sizeof(relation.message_id));
+ if (decoded_type <= 0 || decoded_type >= (int)sizeof(type)
+ || decoded_id != TINY_MESSAGE_ID_LENGTH)
+ {
+ return false;
+ }
+ type[decoded_type] = '\0';
+ relation.message_id[decoded_id] = '\0';
+ if (!tiny_relation_type_parse(type, &relation.type)
+ || !tiny_message_id_valid(relation.message_id))
+ {
+ return false;
+ }
+ resized = realloc(message->metadata.relations,
+ (message->metadata.relation_count + 1) * sizeof(*resized));
+ if (!resized)
+ {
+ return false;
+ }
+ message->metadata.relations = resized;
+ message->metadata.relations[message->metadata.relation_count++] = relation;
+ }
+ }
+ if (sources_type == MJSON_TOK_ARRAY)
+ {
+ offset = 0;
+ while ((offset = mjson_next(sources,
+ sources_len,
+ offset,
+ &key_offset,
+ &key_length,
+ &value_offset,
+ &value_length,
+ &value_type))
+ != 0)
+ {
+ char *id;
+ char **resized;
+ int decoded;
+ if (value_type != MJSON_TOK_STRING
+ || message->metadata.source_count >= TINY_SOURCE_COUNT_MAX)
+ {
+ return false;
+ }
+ id = malloc(TINY_MESSAGE_ID_LENGTH + 1);
+ if (!id)
+ {
+ return false;
+ }
+ decoded = mjson_get_string(
+ sources + value_offset, value_length, "$", id, TINY_MESSAGE_ID_LENGTH + 1);
+ if (decoded != TINY_MESSAGE_ID_LENGTH)
+ {
+ free(id);
+ return false;
+ }
+ id[decoded] = '\0';
+ if (!tiny_message_id_valid(id))
+ {
+ free(id);
+ return false;
+ }
+ resized = realloc(message->metadata.source_message_ids,
+ (message->metadata.source_count + 1) * sizeof(*resized));
+ if (!resized)
+ {
+ free(id);
+ return false;
+ }
+ message->metadata.source_message_ids = resized;
+ message->metadata.source_message_ids[message->metadata.source_count++] = id;
+ }
+ }
+ if (importance_type == MJSON_TOK_NUMBER
+ && mjson_get_number(json, length, "$.importance", &importance) > 0)
+ {
+ if (importance < 0 || importance > 100 || importance != (double)(uint8_t)importance)
+ {
+ return false;
+ }
+ message->metadata.importance = (uint8_t)importance;
+ }
+ if (kind_type == MJSON_TOK_STRING)
+ {
+ char value[24];
+ int decoded = mjson_get_string(json, length, "$.memory_kind", value, sizeof(value));
+ if (decoded <= 0 || decoded >= (int)sizeof(value))
+ {
+ return false;
+ }
+ value[decoded] = '\0';
+ if (!tiny_kind_parse(value, &message->metadata.memory_kind))
+ {
+ return false;
+ }
+ }
+ if (summary_type == MJSON_TOK_STRING)
+ {
+ int decoded;
+ message->metadata.summary = malloc(TINY_SUMMARY_MAX + 1);
+ if (!message->metadata.summary)
+ {
+ return false;
+ }
+ decoded = mjson_get_string(
+ json, length, "$.summary", message->metadata.summary, TINY_SUMMARY_MAX + 1);
+ if (decoded < 0 || decoded > TINY_SUMMARY_MAX
+ || (decoded && !tiny_utf8_valid(message->metadata.summary, (size_t)decoded)))
+ {
+ return false;
+ }
+ message->metadata.summary[decoded] = '\0';
+ if (decoded == 0)
+ {
+ free(message->metadata.summary);
+ message->metadata.summary = NULL;
+ }
+ }
if (tool_call_type == MJSON_TOK_STRING)
{
int decoded;
@@ -144,7 +316,11 @@ parse_metadata(const char *json, tiny_message_t *message)
message->metadata.tags[message->metadata.tag_count++] = tag;
}
}
- return true;
+ if (present)
+ {
+ *present = fields;
+ }
+ return tiny_metadata_valid(&message->metadata);
}
static void
@@ -163,7 +339,7 @@ call_write_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
metadata = tiny_mcp_tool_get_string(tool, "metadata_json", TINY_CONTENT_MAX);
bool valid = context && input.session_id && input.session_id[0] && role && input.content
&& input.content[0] && tiny_role_parse(role, &input.role)
- && parse_metadata(metadata ? metadata : "{}", &input);
+ && tiny_mcp_parse_metadata(metadata ? metadata : "{}", &input, NULL);
if (!valid)
{
tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid write_message arguments");
@@ -178,21 +354,7 @@ call_write_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
}
else
{
- if (context->search_index
- && !tiny_search_index_add(
- context->search_index, &written, tiny_store_entry_count(context->store) - 1))
- {
- tiny_search_index_free(context->search_index);
- context->search_index = NULL;
- }
- if (context->history_index
- && !tiny_history_index_add(context->history_index,
- context->store,
- tiny_store_entry_count(context->store) - 1))
- {
- tiny_history_index_free(context->history_index);
- context->history_index = NULL;
- }
+ tiny_mcp_tools_invalidate_indexes(context);
tiny_json_init(&json);
tiny_json_raw(&json, "{\"status\":\"OK\",\"message\":");
tiny_mcp_tool_append_message(&json, &written);
@@ -225,7 +387,9 @@ tiny_mcp_register_write_message(mcpc_server_t *server)
&& tiny_mcp_tool_add_property(tool,
"metadata_json",
"Optional JSON object string containing tool_call_id, tags, "
- "and/or memory_tier; defaults to {}.",
+ "memory_tier, memory_kind, importance, summary, relations, "
+ "and/or source_message_ids; "
+ "defaults to {}.",
MCPC_U8STR,
false)
&& mcpc_server_add_tool(server, tool) == MCPC_EC_0;
diff --git a/src/storage/store.c b/src/storage/store.c
index 296f4d0..d798d86 100644
--- a/src/storage/store.c
+++ b/src/storage/store.c
@@ -9,10 +9,14 @@
#include "base/allocator.h"
#define RECORD_HEADER_SIZE 24U
-#define RECORD_VERSION 1U
+#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
@@ -27,6 +31,8 @@ struct tiny_store
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];
@@ -180,6 +186,7 @@ serialize_put(const tiny_message_t *message, unsigned char **payload, uint32_t *
{
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;
@@ -187,6 +194,8 @@ serialize_put(const tiny_message_t *message, unsigned char **payload, uint32_t *
{
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;
@@ -228,6 +237,36 @@ serialize_put(const tiny_message_t *message, unsigned char **payload, uint32_t *
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;
@@ -330,12 +369,95 @@ deserialize_put(const unsigned char *data, uint32_t length, tiny_message_t *mess
message->metadata.tag_count++;
}
}
- if (offset != length || !tiny_utf8_valid(message->session_id, session_len)
+ if (!tiny_utf8_valid(message->session_id, session_len)
|| !tiny_utf8_valid(message->content, content_len))
{
return deserialize_failed(message);
}
- return true;
+ 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
@@ -381,6 +503,545 @@ apply_put(tiny_store_t *store, tiny_message_t *message, uint64_t 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;
+ if (!tiny_metadata_valid(metadata))
+ {
+ return false;
+ }
+ for (index = 0; index < metadata->relation_count; ++index)
+ {
+ tiny_store_entry_t *target;
+ if (self_id && strcmp(self_id, metadata->relations[index].message_id) == 0)
+ {
+ return false;
+ }
+ target = find_entry(store, metadata->relations[index].message_id);
+ if (!target || target->deleted)
+ {
+ return false;
+ }
+ }
+ for (index = 0; index < metadata->source_count; ++index)
+ {
+ tiny_store_entry_t *target;
+ if (self_id && strcmp(self_id, metadata->source_message_ids[index]) == 0)
+ {
+ return false;
+ }
+ target = find_entry(store, metadata->source_message_ids[index]);
+ if (!target || target->deleted)
+ {
+ return false;
+ }
+ }
+ 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)
{
@@ -431,8 +1092,10 @@ replay_log(tiny_store_t *store, uint64_t starting_offset)
{
return recover_torn_tail(store, boundary);
}
- if (memcmp(header, "TCMR", 4) != 0 || header[4] != RECORD_VERSION
- || (header[5] != RECORD_PUT && header[5] != RECORD_DEL && header[5] != RECORD_MARKER))
+ 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;
}
@@ -507,9 +1170,28 @@ replay_log(tiny_store_t *store, uint64_t starting_offset)
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;
@@ -596,8 +1278,10 @@ log_generation_matches(tiny_store_t *store, const unsigned char generation[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[5] != RECORD_MARKER
- || tiny_codec_get_u64(header + 12) != 0 || tiny_codec_get_u32(header + 8) != 16
+ || 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)))
{
@@ -799,6 +1483,8 @@ tiny_store_status_name(tiny_store_status_t status)
return "IO_ERROR";
case TINY_STORE_CORRUPT:
return "CORRUPT_STORE";
+ case TINY_STORE_CONFLICT:
+ return "CONFLICT";
default:
return "INTERNAL_ERROR";
}
@@ -820,6 +1506,12 @@ tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_
{
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)
@@ -827,6 +1519,8 @@ tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_
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)
@@ -856,6 +1550,127 @@ tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_
}
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;
@@ -882,6 +1697,7 @@ tiny_store_delete(tiny_store_t *store, const char *message_id, const char *sessi
}
store->last_sequence++;
entry->deleted = true;
+ store->dead_bytes += RECORD_HEADER_SIZE + sizeof(payload);
return TINY_STORE_OK;
}
@@ -905,6 +1721,15 @@ tiny_store_rebuild(tiny_store_t *store, tiny_rebuild_report_t *report)
{
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");
@@ -1002,6 +1827,11 @@ tiny_store_rebuild(tiny_store_t *store, tiny_rebuild_report_t *report)
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)
{
@@ -1032,6 +1862,7 @@ tiny_store_health(const tiny_store_t *store, tiny_store_health_t *health)
{
return;
}
+ health->format_version = RECORD_VERSION;
if (tiny_sys_file_size(&store->log, &size))
{
health->log_size = size;
@@ -1059,6 +1890,17 @@ tiny_store_health(const tiny_store_t *store, tiny_store_health_t *health)
}
}
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);
diff --git a/src/storage/store.h b/src/storage/store.h
index 619f86f..3ddd24d 100644
--- a/src/storage/store.h
+++ b/src/storage/store.h
@@ -14,7 +14,8 @@ typedef enum tiny_store_status
TINY_STORE_BUSY,
TINY_STORE_IO_ERROR,
TINY_STORE_CORRUPT,
- TINY_STORE_NO_MEMORY
+ TINY_STORE_NO_MEMORY,
+ TINY_STORE_CONFLICT
} tiny_store_status_t;
typedef struct tiny_store_entry
@@ -36,16 +37,33 @@ typedef struct tiny_rebuild_report
typedef struct tiny_store_health
{
+ uint32_t format_version;
uint64_t log_size;
uint64_t snapshot_size;
uint64_t active_messages;
uint64_t deleted_messages;
uint64_t last_sequence;
+ uint64_t update_records;
+ uint64_t dead_bytes;
+ uint64_t write_bytes;
+ uint64_t write_calls;
+ uint64_t flush_calls;
+ uint64_t truncate_calls;
+ uint64_t replace_calls;
bool recovered_tail;
bool snapshot_valid;
char last_error[160];
} tiny_store_health_t;
+typedef struct tiny_message_patch
+{
+ const char *content;
+ const tiny_metadata_t *metadata;
+ tiny_metadata_mask_t metadata_mask;
+ uint64_t expected_revision;
+ bool has_expected_revision;
+} tiny_message_patch_t;
+
tiny_store_status_t tiny_store_open(tiny_store_t **store, const char *directory);
void tiny_store_close(tiny_store_t *store);
const char *tiny_store_status_name(tiny_store_status_t status);
@@ -54,6 +72,11 @@ tiny_store_status_t
tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_t *written);
tiny_store_status_t
tiny_store_delete(tiny_store_t *store, const char *message_id, const char *session_id);
+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_status_t tiny_store_rebuild(tiny_store_t *store, tiny_rebuild_report_t *report);
void tiny_store_health(const tiny_store_t *store, tiny_store_health_t *health);
diff --git a/src/sys/system.c b/src/sys/system.c
index 63d8297..0d99b28 100644
--- a/src/sys/system.c
+++ b/src/sys/system.c
@@ -16,6 +16,22 @@ typedef struct tiny_sys_test_state
} tiny_sys_test_state_t;
static tiny_sys_test_state_t tiny_sys_test_state;
+static tiny_sys_io_counters_t tiny_sys_io_counters;
+
+void
+tiny_sys_io_counters_reset(void)
+{
+ memset(&tiny_sys_io_counters, 0, sizeof(tiny_sys_io_counters));
+}
+
+void
+tiny_sys_io_counters_get(tiny_sys_io_counters_t *counters)
+{
+ if (counters != NULL)
+ {
+ *counters = tiny_sys_io_counters;
+ }
+}
void
tiny_sys_test_reset(void)
@@ -349,6 +365,11 @@ tiny_sys_file_write(tiny_sys_file_t *file, const void *data, uint32_t length, ui
length = tiny_sys_test_transfer_length(length);
result = WriteFile(native_file(file), data, length, &count, NULL) != 0;
*actual = count;
+ if (result)
+ {
+ tiny_sys_io_counters.write_calls++;
+ tiny_sys_io_counters.write_bytes += count;
+ }
return result;
}
@@ -400,15 +421,19 @@ tiny_sys_file_size(const tiny_sys_file_t *file, uint64_t *size)
bool
tiny_sys_file_truncate(tiny_sys_file_t *file)
{
- return tiny_sys_file_valid(file) && !tiny_sys_test_should_fail(TINY_SYS_TEST_TRUNCATE)
- && SetEndOfFile(native_file(file)) != 0;
+ bool result = tiny_sys_file_valid(file) && !tiny_sys_test_should_fail(TINY_SYS_TEST_TRUNCATE)
+ && SetEndOfFile(native_file(file)) != 0;
+ tiny_sys_io_counters.truncate_calls += result ? 1U : 0U;
+ return result;
}
bool
tiny_sys_file_flush(tiny_sys_file_t *file)
{
- return tiny_sys_file_valid(file) && !tiny_sys_test_should_fail(TINY_SYS_TEST_FLUSH)
- && FlushFileBuffers(native_file(file)) != 0;
+ bool result = tiny_sys_file_valid(file) && !tiny_sys_test_should_fail(TINY_SYS_TEST_FLUSH)
+ && FlushFileBuffers(native_file(file)) != 0;
+ tiny_sys_io_counters.flush_calls += result ? 1U : 0U;
+ return result;
}
bool
@@ -427,6 +452,7 @@ tiny_sys_path_replace(const char *source, const char *destination)
}
free(wide_source);
free(wide_destination);
+ tiny_sys_io_counters.replace_calls += result ? 1U : 0U;
return result;
}
@@ -703,6 +729,11 @@ tiny_sys_file_read(tiny_sys_file_t *file, void *data, uint32_t length, uint32_t
}
while (count < 0 && errno == EINTR);
*actual = count > 0 ? (uint32_t)count : 0;
+ if (count >= 0)
+ {
+ tiny_sys_io_counters.write_calls++;
+ tiny_sys_io_counters.write_bytes += *actual;
+ }
return count >= 0;
}
@@ -769,21 +800,31 @@ tiny_sys_file_truncate(tiny_sys_file_t *file)
return false;
}
position = lseek(native_file(file), 0, SEEK_CUR);
- return position >= 0 && ftruncate(native_file(file), position) == 0;
+ if (position >= 0 && ftruncate(native_file(file), position) == 0)
+ {
+ tiny_sys_io_counters.truncate_calls++;
+ return true;
+ }
+ return false;
}
bool
tiny_sys_file_flush(tiny_sys_file_t *file)
{
- return tiny_sys_file_valid(file) && !tiny_sys_test_should_fail(TINY_SYS_TEST_FLUSH)
- && fsync(native_file(file)) == 0;
+ bool result = tiny_sys_file_valid(file) && !tiny_sys_test_should_fail(TINY_SYS_TEST_FLUSH)
+ && fsync(native_file(file)) == 0;
+ tiny_sys_io_counters.flush_calls += result ? 1U : 0U;
+ return result;
}
bool
tiny_sys_path_replace(const char *source, const char *destination)
{
- return source != NULL && destination != NULL && !tiny_sys_test_should_fail(TINY_SYS_TEST_REPLACE)
- && rename(source, destination) == 0;
+ bool result = source != NULL && destination != NULL
+ && !tiny_sys_test_should_fail(TINY_SYS_TEST_REPLACE)
+ && rename(source, destination) == 0;
+ tiny_sys_io_counters.replace_calls += result ? 1U : 0U;
+ return result;
}
bool
diff --git a/src/sys/system.h b/src/sys/system.h
index b526ec5..0947205 100644
--- a/src/sys/system.h
+++ b/src/sys/system.h
@@ -40,6 +40,18 @@ typedef struct tiny_sys_arguments
char **values;
} tiny_sys_arguments_t;
+typedef struct tiny_sys_io_counters
+{
+ uint64_t write_bytes;
+ uint64_t write_calls;
+ uint64_t flush_calls;
+ uint64_t truncate_calls;
+ uint64_t replace_calls;
+} tiny_sys_io_counters_t;
+
+void tiny_sys_io_counters_reset(void);
+void tiny_sys_io_counters_get(tiny_sys_io_counters_t *counters);
+
/* Internal test seam. A call index of 1 fails the next matching operation. */
typedef enum tiny_sys_test_operation
{