summaryrefslogtreecommitdiff
path: root/src/mcp/tools/read_messages.c
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-17 12:15:41 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-17 12:15:41 +0900
commitd57577637c1c1a16176d63d5a89e9eec3c1e4d7c (patch)
tree55087498ab17601845446174a2f41f2490a53820 /src/mcp/tools/read_messages.c
parentdda043b0ec9d8d9e5da7fe51fc48791086417a9b (diff)
fix: sessions paginationとhealth診断を改善
- sessions cursorのエンコード不具合と長いsession IDのバッファ問題を修正 - response budgetに基づくsessionsページングを追加 - get_system_healthにsession_countを追加 - metadata_jsonのフィールド別エラー診断を追加 - CRUD互換性とDebug/Release/ASanテストを確認
Diffstat (limited to 'src/mcp/tools/read_messages.c')
-rw-r--r--src/mcp/tools/read_messages.c185
1 files changed, 120 insertions, 65 deletions
diff --git a/src/mcp/tools/read_messages.c b/src/mcp/tools/read_messages.c
index 109becb..131c513 100644
--- a/src/mcp/tools/read_messages.c
+++ b/src/mcp/tools/read_messages.c
@@ -15,8 +15,10 @@
#define READ_DEFAULT_LIMIT 100
#define READ_MAX_LIMIT 1000
#define READ_RESPONSE_BUDGET (8U * 1024U * 1024U - 64U * 1024U)
-#define CURSOR_BYTES 47U
-#define CURSOR_TEXT_MAX 64U
+#define HISTORY_CURSOR_BYTES 47U
+#define HISTORY_CURSOR_TEXT_MAX 64U
+#define SESSION_CURSOR_BYTES_MAX (1U + 8U + 4U + TINY_SESSION_ID_MAX + 4U)
+#define SESSION_CURSOR_TEXT_MAX (((SESSION_CURSOR_BYTES_MAX * 4U + 2U) / 3U) + 1U)
typedef struct history_cursor
{
@@ -101,16 +103,16 @@ base64_value(unsigned char value)
}
static bool
-base64url_decode(const char *input, unsigned char output[CURSOR_BYTES])
+base64url_decode_n(const char *input, unsigned char *output, size_t capacity, size_t *length)
{
- size_t length = strlen(input), source = 0, target = 0;
+ size_t input_length = strlen(input), source = 0, target = 0;
uint32_t accumulator = 0;
unsigned bits = 0;
- if (length != 63)
+ if (!output || !length || input_length == 0 || input_length % 4 == 1)
{
return false;
}
- while (source < length)
+ while (source < input_length)
{
int value = base64_value((unsigned char)input[source++]);
if (value < 0)
@@ -122,7 +124,7 @@ base64url_decode(const char *input, unsigned char output[CURSOR_BYTES])
if (bits >= 8)
{
bits -= 8;
- if (target >= CURSOR_BYTES)
+ if (target >= capacity)
{
return false;
}
@@ -130,26 +132,31 @@ base64url_decode(const char *input, unsigned char output[CURSOR_BYTES])
accumulator &= bits ? (UINT32_C(1) << bits) - 1 : 0;
}
}
- return target == CURSOR_BYTES && accumulator == 0;
+ *length = target;
+ return accumulator == 0;
}
static bool
-cursor_encode(const char *session, const tiny_message_t *message, char output[CURSOR_TEXT_MAX])
+cursor_encode(const char *session,
+ const tiny_message_t *message,
+ char output[HISTORY_CURSOR_TEXT_MAX])
{
- unsigned char data[CURSOR_BYTES] = { 0 };
+ unsigned char data[HISTORY_CURSOR_BYTES] = { 0 };
data[0] = 1;
tiny_codec_put_u64(data + 1, session_hash(session));
tiny_codec_put_u64(data + 9, (uint64_t)message->timestamp_ms);
memcpy(data + 17, message->message_id, 26);
tiny_codec_put_u32(data + 43, tiny_codec_crc32(data, 43));
- return base64url_encode(data, sizeof(data), output, CURSOR_TEXT_MAX) == 63;
+ return base64url_encode(data, sizeof(data), output, HISTORY_CURSOR_TEXT_MAX) == 63;
}
static bool
cursor_decode(const char *session, const char *value, history_cursor_t *cursor)
{
- unsigned char data[CURSOR_BYTES];
- if (!value || !value[0] || !base64url_decode(value, data) || data[0] != 1
+ unsigned char data[HISTORY_CURSOR_BYTES];
+ size_t length = 0;
+ if (!value || strlen(value) != 63 || !base64url_decode_n(value, data, sizeof(data), &length)
+ || length != HISTORY_CURSOR_BYTES || data[0] != 1
|| tiny_codec_get_u64(data + 1) != session_hash(session)
|| tiny_codec_get_u32(data + 43) != tiny_codec_crc32(data, 43))
{
@@ -538,16 +545,24 @@ simple_cursor(const char *cursor, bool *valid)
}
static bool
-session_cursor_encode(int64_t last_message_at, const char *session_id, char output[CURSOR_TEXT_MAX])
+session_cursor_encode(int64_t last_message_at,
+ const char *session_id,
+ char output[SESSION_CURSOR_TEXT_MAX])
{
- unsigned char data[CURSOR_BYTES] = { 0 };
+ unsigned char data[SESSION_CURSOR_BYTES_MAX] = { 0 };
+ size_t session_length = strlen(session_id);
+ size_t data_length;
+ if (session_length == 0 || session_length > TINY_SESSION_ID_MAX)
+ {
+ return false;
+ }
+ data_length = 1U + 8U + 4U + session_length + 4U;
data[0] = 2;
tiny_codec_put_u64(data + 1, (uint64_t)last_message_at);
- tiny_codec_put_u32(data + 9,
- tiny_codec_crc32((const unsigned char *)session_id, strlen(session_id)));
- memcpy(data + 13, session_id, strlen(session_id));
- tiny_codec_put_u32(data + 43, tiny_codec_crc32(data, 43));
- return base64url_encode(data, sizeof(data), output, CURSOR_TEXT_MAX) == 63;
+ tiny_codec_put_u32(data + 9, (uint32_t)session_length);
+ memcpy(data + 13, session_id, session_length);
+ tiny_codec_put_u32(data + data_length - 4U, tiny_codec_crc32(data, data_length - 4U));
+ return base64url_encode(data, data_length, output, SESSION_CURSOR_TEXT_MAX) > 0;
}
static bool
@@ -555,22 +570,25 @@ session_cursor_decode(const char *cursor,
int64_t *last_message_at,
char session_id[TINY_SESSION_ID_MAX + 1])
{
- unsigned char data[CURSOR_BYTES];
- size_t length;
- if (!cursor || !cursor[0] || !base64url_decode(cursor, data) || data[0] != 2)
+ unsigned char data[SESSION_CURSOR_BYTES_MAX];
+ size_t decoded_length = 0, session_length;
+ if (!cursor || !cursor[0] || !base64url_decode_n(cursor, data, sizeof(data), &decoded_length)
+ || decoded_length < 18U || data[0] != 2)
{
return false;
}
*last_message_at = (int64_t)tiny_codec_get_u64(data + 1);
- length = tiny_codec_get_u32(data + 9);
- if (length == 0 || length > TINY_SESSION_ID_MAX
- || tiny_codec_crc32(data, 43) != tiny_codec_get_u32(data + 43))
+ session_length = tiny_codec_get_u32(data + 9);
+ if (*last_message_at < 0 || session_length == 0 || session_length > TINY_SESSION_ID_MAX
+ || decoded_length != 1U + 8U + 4U + session_length + 4U
+ || tiny_codec_crc32(data, decoded_length - 4U)
+ != tiny_codec_get_u32(data + decoded_length - 4U))
{
return false;
}
- memcpy(session_id, data + 13, length);
- session_id[length] = '\0';
- return tiny_utf8_valid(session_id, length);
+ memcpy(session_id, data + 13, session_length);
+ session_id[session_length] = '\0';
+ return tiny_utf8_valid(session_id, session_length);
}
static int
@@ -588,14 +606,15 @@ compare_session_by_last_then_id(const void *left, const void *right)
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);
+ char *cursor = tiny_mcp_tool_get_string(tool, "cursor", SESSION_CURSOR_TEXT_MAX - 1U);
int32_t limit = READ_DEFAULT_LIMIT;
tiny_json_buffer_t json;
mcpc_errcode_t limit_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &limit);
int64_t cursor_last = 0;
char cursor_session[TINY_SESSION_ID_MAX + 1] = { 0 };
bool has_cursor = cursor && cursor[0];
- size_t emitted = 0, index, position;
+ size_t emitted = 0, index, position, last_emitted_position = SIZE_MAX, outer_size = 256;
+ bool more = false;
const tiny_history_index_t *history;
tiny_history_index_session_info_t *sessions = NULL;
size_t session_count = 0;
@@ -680,49 +699,85 @@ call_sessions_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, m
{
continue;
}
- if (emitted++)
- {
- tiny_json_raw(&json, ",");
- }
summary = representative->message.metadata.summary ? representative->message.metadata.summary
: representative->message.content;
- tiny_json_raw(&json, "{\"session_id\":");
- tiny_json_string(&json, session_id);
- tiny_json_raw(&json, ",\"message_count\":");
- tiny_json_u64(&json, count);
- tiny_json_raw(&json, ",\"kind_counts\":{\"conversation\":");
- tiny_json_i64(&json, kind_counts[0]);
- tiny_json_raw(&json, ",\"episode\":");
- tiny_json_i64(&json, kind_counts[1]);
- tiny_json_raw(&json, ",\"semantic\":");
- tiny_json_i64(&json, kind_counts[2]);
- tiny_json_raw(&json, ",\"procedure\":");
- tiny_json_i64(&json, kind_counts[3]);
- tiny_json_raw(&json, ",\"preference\":");
- tiny_json_i64(&json, kind_counts[4]);
- tiny_json_raw(&json, ",\"summary\":");
- tiny_json_i64(&json, kind_counts[5]);
- tiny_json_raw(&json, "},\"representative_summary\":");
- tiny_json_string(&json, summary);
- 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_buffer_t item;
+ size_t item_size;
+ tiny_json_init(&item);
+ tiny_json_raw(&item, "{\"session_id\":");
+ tiny_json_string(&item, session_id);
+ tiny_json_raw(&item, ",\"message_count\":");
+ tiny_json_u64(&item, count);
+ tiny_json_raw(&item, ",\"kind_counts\":{\"conversation\":");
+ tiny_json_i64(&item, kind_counts[0]);
+ tiny_json_raw(&item, ",\"episode\":");
+ tiny_json_i64(&item, kind_counts[1]);
+ tiny_json_raw(&item, ",\"semantic\":");
+ tiny_json_i64(&item, kind_counts[2]);
+ tiny_json_raw(&item, ",\"procedure\":");
+ tiny_json_i64(&item, kind_counts[3]);
+ tiny_json_raw(&item, ",\"preference\":");
+ tiny_json_i64(&item, kind_counts[4]);
+ tiny_json_raw(&item, ",\"summary\":");
+ tiny_json_i64(&item, kind_counts[5]);
+ tiny_json_raw(&item, "},\"representative_summary\":");
+ tiny_json_string(&item, summary);
+ tiny_json_raw(&item, ",\"started_at\":");
+ tiny_json_i64(&item, started);
+ tiny_json_raw(&item, ",\"last_message_at\":");
+ tiny_json_i64(&item, ended);
+ tiny_json_raw(&item, "}");
+ item_size = item.failed ? 0U : escaped_size(item.data, item.length) + (emitted ? 2U : 0U);
+ if (item.failed || (emitted && outer_size + item_size > READ_RESPONSE_BUDGET))
+ {
+ if (item.failed)
+ {
+ json.failed = true;
+ }
+ else
+ {
+ more = 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);
+ outer_size += item_size;
+ emitted++;
+ last_emitted_position = position;
+ }
}
tiny_json_raw(&json, "]");
- if (emitted == (size_t)limit && position < session_count)
+ if (!more && emitted == (size_t)limit && position < session_count)
{
- char next[CURSOR_TEXT_MAX];
- if (session_cursor_encode(
- sessions[position - 1].last_message_at, sessions[position - 1].session_id, next))
+ more = true;
+ }
+ if (more && last_emitted_position != SIZE_MAX)
+ {
+ char next[SESSION_CURSOR_TEXT_MAX];
+ if (session_cursor_encode(sessions[last_emitted_position].last_message_at,
+ sessions[last_emitted_position].session_id,
+ next))
{
tiny_json_raw(&json, ",\"nextCursor\":");
tiny_json_string(&json, next);
}
}
tiny_json_raw(&json, "}");
- tiny_mcp_tool_add_json(result, &json);
+ if (json.failed)
+ {
+ tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "sessions response allocation failed");
+ }
+ else
+ {
+ tiny_mcp_tool_add_json(result, &json);
+ }
tiny_json_free(&json);
free(sessions);
free(cursor);
@@ -734,7 +789,7 @@ 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);
+ char *cursor_text = tiny_mcp_tool_get_string(tool, "cursor", SESSION_CURSOR_TEXT_MAX - 1U);
int32_t requested_limit = READ_DEFAULT_LIMIT;
mcpc_errcode_t limit_status
= mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &requested_limit);
@@ -860,7 +915,7 @@ call_read_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result)
tiny_json_raw(&json, "]");
if (more && last)
{
- char next_cursor[CURSOR_TEXT_MAX];
+ char next_cursor[HISTORY_CURSOR_TEXT_MAX];
if (!cursor_encode(session, last, next_cursor))
{
json.failed = true;