From d57577637c1c1a16176d63d5a89e9eec3c1e4d7c Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Fri, 17 Jul 2026 12:15:41 +0900 Subject: fix: sessions paginationとhealth診断を改善 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sessions cursorのエンコード不具合と長いsession IDのバッファ問題を修正 - response budgetに基づくsessionsページングを追加 - get_system_healthにsession_countを追加 - metadata_jsonのフィールド別エラー診断を追加 - CRUD互換性とDebug/Release/ASanテストを確認 --- src/mcp/tools/read_messages.c | 185 +++++++++++++++++++++++++++--------------- 1 file changed, 120 insertions(+), 65 deletions(-) (limited to 'src/mcp/tools/read_messages.c') 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; -- cgit v1.2.3