From 8451dc0efd5b40e3ca62af28aff4aa71bac72f60 Mon Sep 17 00:00:00 2001 From: STHITAPRAJNAS Date: Fri, 27 Mar 2026 01:59:02 +0000 Subject: [PATCH 1/3] fix(sessions): truncate error_message in v0 StorageEvent to guard against VARCHAR(255) overflow Databases created with earlier ADK versions retain error_message as VARCHAR(255) because create_all() is additive-only and never ALTERs existing columns. When a model returns a MALFORMED_FUNCTION_CALL error with a large JSON payload, the unbounded error_message silently exceeds that limit and raises StringDataRightTruncationError, terminating the SSE stream with no fallback. The fix adds a write-path guard in StorageEvent.from_event(): if the message exceeds 255 characters it is clipped to 241 chars and suffixed with "...[truncated]", keeping the total within the legacy column width. Messages within the limit are passed through unchanged. The v1 schema is unaffected (error_message is stored inside the event_data JSONB column). The permanent fix remains running: ALTER TABLE events ALTER COLUMN error_message TYPE TEXT; or migrating to the v1 schema via `adk migrate session`. Fixes #4993 --- src/google/adk/sessions/schemas/v0.py | 17 ++++++++- .../sessions/test_v0_storage_event.py | 38 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/google/adk/sessions/schemas/v0.py b/src/google/adk/sessions/schemas/v0.py index 7679a56e5b..2be3f82ef1 100644 --- a/src/google/adk/sessions/schemas/v0.py +++ b/src/google/adk/sessions/schemas/v0.py @@ -61,6 +61,13 @@ from .shared import DynamicJSON from .shared import PreciseTimestamp +# Legacy v0 databases may retain error_message as VARCHAR(255) from earlier +# schema versions. create_all() is additive only, so existing columns are never +# altered. Truncate on write to avoid StringDataRightTruncationError on those +# databases. +_LEGACY_ERROR_MSG_MAX_LEN = 255 +_TRUNCATION_SUFFIX = "...[truncated]" + class DynamicPickleType(TypeDecorator): """Represents a type that can be pickled.""" @@ -298,7 +305,15 @@ def from_event(cls, session: Session, event: Event) -> StorageEvent: partial=event.partial, turn_complete=event.turn_complete, error_code=event.error_code, - error_message=event.error_message, + error_message=( + event.error_message[ + : _LEGACY_ERROR_MSG_MAX_LEN - len(_TRUNCATION_SUFFIX) + ] + + _TRUNCATION_SUFFIX + if event.error_message + and len(event.error_message) > _LEGACY_ERROR_MSG_MAX_LEN + else event.error_message + ), interrupted=event.interrupted, ) if event.content: diff --git a/tests/unittests/sessions/test_v0_storage_event.py b/tests/unittests/sessions/test_v0_storage_event.py index cc82272506..3d936f145c 100644 --- a/tests/unittests/sessions/test_v0_storage_event.py +++ b/tests/unittests/sessions/test_v0_storage_event.py @@ -15,9 +15,13 @@ from datetime import datetime from datetime import timezone +from google.adk.events.event import Event from google.adk.events.event_actions import EventActions from google.adk.events.event_actions import EventCompaction from google.adk.sessions.schemas.v0 import StorageEvent +from google.adk.sessions.schemas.v0 import _LEGACY_ERROR_MSG_MAX_LEN +from google.adk.sessions.schemas.v0 import _TRUNCATION_SUFFIX +from google.adk.sessions.session import Session from google.genai import types @@ -48,3 +52,37 @@ def test_storage_event_v0_to_event_rehydrates_compaction_model(): assert isinstance(event.actions.compaction, EventCompaction) assert event.actions.compaction.start_timestamp == 1.0 assert event.actions.compaction.end_timestamp == 2.0 + + +def test_from_event_truncates_error_message_exceeding_varchar255(): + session = Session(app_name="app", user_id="user", id="session_id") + event = Event( + id="event_id", + invocation_id="invocation_id", + author="author", + timestamp=1.0, + error_code="MALFORMED_FUNCTION_CALL", + error_message="x" * 300, + ) + + storage_event = StorageEvent.from_event(session, event) + + assert len(storage_event.error_message) <= _LEGACY_ERROR_MSG_MAX_LEN + assert storage_event.error_message.endswith(_TRUNCATION_SUFFIX) + + +def test_from_event_preserves_short_error_message(): + session = Session(app_name="app", user_id="user", id="session_id") + short_message = "Malformed function call" + event = Event( + id="event_id", + invocation_id="invocation_id", + author="author", + timestamp=1.0, + error_code="MALFORMED_FUNCTION_CALL", + error_message=short_message, + ) + + storage_event = StorageEvent.from_event(session, event) + + assert storage_event.error_message == short_message From 9966d441fb43118081a3b178b2f2dfec14d1db2b Mon Sep 17 00:00:00 2001 From: STHITAPRAJNAS Date: Fri, 27 Mar 2026 13:07:51 +0000 Subject: [PATCH 2/3] test(sessions): tighten v0 error_message truncation assertions per review - Replace <= with == in the truncation test: the guard is deterministic so the result should be exactly 255 chars, not merely at most 255 - Assert the concrete expected string ("x" * 241 + "...[truncated]") rather than importing private module symbols, reducing coupling to internal implementation details - Add test_from_event_with_none_error_message to cover the falsy branch of the truncation condition and document that None passes through unchanged --- .../sessions/test_v0_storage_event.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/unittests/sessions/test_v0_storage_event.py b/tests/unittests/sessions/test_v0_storage_event.py index 3d936f145c..995ec520bd 100644 --- a/tests/unittests/sessions/test_v0_storage_event.py +++ b/tests/unittests/sessions/test_v0_storage_event.py @@ -19,8 +19,6 @@ from google.adk.events.event_actions import EventActions from google.adk.events.event_actions import EventCompaction from google.adk.sessions.schemas.v0 import StorageEvent -from google.adk.sessions.schemas.v0 import _LEGACY_ERROR_MSG_MAX_LEN -from google.adk.sessions.schemas.v0 import _TRUNCATION_SUFFIX from google.adk.sessions.session import Session from google.genai import types @@ -67,8 +65,7 @@ def test_from_event_truncates_error_message_exceeding_varchar255(): storage_event = StorageEvent.from_event(session, event) - assert len(storage_event.error_message) <= _LEGACY_ERROR_MSG_MAX_LEN - assert storage_event.error_message.endswith(_TRUNCATION_SUFFIX) + assert storage_event.error_message == "x" * 241 + "...[truncated]" def test_from_event_preserves_short_error_message(): @@ -86,3 +83,17 @@ def test_from_event_preserves_short_error_message(): storage_event = StorageEvent.from_event(session, event) assert storage_event.error_message == short_message + + +def test_from_event_with_none_error_message(): + session = Session(app_name="app", user_id="user", id="session_id") + event = Event( + id="event_id", + invocation_id="invocation_id", + author="author", + timestamp=1.0, + ) + + storage_event = StorageEvent.from_event(session, event) + + assert storage_event.error_message is None From 8d65e923d7c3b0ecbbf6a3cc9584f1c157dbad8f Mon Sep 17 00:00:00 2001 From: STHITAPRAJNAS Date: Tue, 31 Mar 2026 05:28:33 +0000 Subject: [PATCH 3/3] fix(sessions): emit logger.warning with full message before truncating error_message Truncation is a lossy operation. Without surfacing the original content, users hitting this path on a legacy VARCHAR(255) schema had no way to recover the full error text without attaching a debugger. Extract the truncation logic into a pre-flight block before the StorageEvent constructor, emit a WARNING with the event id, the length limit, and the full original message, then pass the already-truncated value into the constructor. This makes the information available at WARNING level in any standard logging setup. Also adds import logging and module-level logger following the google_adk. convention already used in the sessions package. Test updated to assert the warning is emitted and contains the original full message. --- src/google/adk/sessions/schemas/v0.py | 26 ++++++++++++------- .../sessions/test_v0_storage_event.py | 9 +++++-- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/google/adk/sessions/schemas/v0.py b/src/google/adk/sessions/schemas/v0.py index 2be3f82ef1..79e7bdc61b 100644 --- a/src/google/adk/sessions/schemas/v0.py +++ b/src/google/adk/sessions/schemas/v0.py @@ -29,10 +29,13 @@ from datetime import datetime from datetime import timezone import json +import logging import pickle from typing import Any from typing import Optional +logger = logging.getLogger("google_adk." + __name__) + from google.adk.platform import uuid as platform_uuid from google.genai import types from sqlalchemy import Boolean @@ -291,6 +294,19 @@ def long_running_tool_ids(self, value: set[str]): @classmethod def from_event(cls, session: Session, event: Event) -> StorageEvent: + error_message = event.error_message + if error_message and len(error_message) > _LEGACY_ERROR_MSG_MAX_LEN: + logger.warning( + "error_message for event %s exceeds %d characters and will be" + " truncated. Full message: %s", + event.id, + _LEGACY_ERROR_MSG_MAX_LEN, + error_message, + ) + error_message = ( + error_message[: _LEGACY_ERROR_MSG_MAX_LEN - len(_TRUNCATION_SUFFIX)] + + _TRUNCATION_SUFFIX + ) storage_event = StorageEvent( id=event.id, invocation_id=event.invocation_id, @@ -305,15 +321,7 @@ def from_event(cls, session: Session, event: Event) -> StorageEvent: partial=event.partial, turn_complete=event.turn_complete, error_code=event.error_code, - error_message=( - event.error_message[ - : _LEGACY_ERROR_MSG_MAX_LEN - len(_TRUNCATION_SUFFIX) - ] - + _TRUNCATION_SUFFIX - if event.error_message - and len(event.error_message) > _LEGACY_ERROR_MSG_MAX_LEN - else event.error_message - ), + error_message=error_message, interrupted=event.interrupted, ) if event.content: diff --git a/tests/unittests/sessions/test_v0_storage_event.py b/tests/unittests/sessions/test_v0_storage_event.py index 995ec520bd..5889afd50b 100644 --- a/tests/unittests/sessions/test_v0_storage_event.py +++ b/tests/unittests/sessions/test_v0_storage_event.py @@ -52,7 +52,9 @@ def test_storage_event_v0_to_event_rehydrates_compaction_model(): assert event.actions.compaction.end_timestamp == 2.0 -def test_from_event_truncates_error_message_exceeding_varchar255(): +def test_from_event_truncates_error_message_exceeding_varchar255(caplog): + import logging + session = Session(app_name="app", user_id="user", id="session_id") event = Event( id="event_id", @@ -63,9 +65,12 @@ def test_from_event_truncates_error_message_exceeding_varchar255(): error_message="x" * 300, ) - storage_event = StorageEvent.from_event(session, event) + with caplog.at_level(logging.WARNING): + storage_event = StorageEvent.from_event(session, event) assert storage_event.error_message == "x" * 241 + "...[truncated]" + assert any("truncated" in r.message for r in caplog.records) + assert any("x" * 300 in r.message for r in caplog.records) def test_from_event_preserves_short_error_message():