Skip to content

Commit 8451dc0

Browse files
committed
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
1 parent f973673 commit 8451dc0

File tree

2 files changed

+54
-1
lines changed

2 files changed

+54
-1
lines changed

src/google/adk/sessions/schemas/v0.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@
6161
from .shared import DynamicJSON
6262
from .shared import PreciseTimestamp
6363

64+
# Legacy v0 databases may retain error_message as VARCHAR(255) from earlier
65+
# schema versions. create_all() is additive only, so existing columns are never
66+
# altered. Truncate on write to avoid StringDataRightTruncationError on those
67+
# databases.
68+
_LEGACY_ERROR_MSG_MAX_LEN = 255
69+
_TRUNCATION_SUFFIX = "...[truncated]"
70+
6471

6572
class DynamicPickleType(TypeDecorator):
6673
"""Represents a type that can be pickled."""
@@ -298,7 +305,15 @@ def from_event(cls, session: Session, event: Event) -> StorageEvent:
298305
partial=event.partial,
299306
turn_complete=event.turn_complete,
300307
error_code=event.error_code,
301-
error_message=event.error_message,
308+
error_message=(
309+
event.error_message[
310+
: _LEGACY_ERROR_MSG_MAX_LEN - len(_TRUNCATION_SUFFIX)
311+
]
312+
+ _TRUNCATION_SUFFIX
313+
if event.error_message
314+
and len(event.error_message) > _LEGACY_ERROR_MSG_MAX_LEN
315+
else event.error_message
316+
),
302317
interrupted=event.interrupted,
303318
)
304319
if event.content:

tests/unittests/sessions/test_v0_storage_event.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@
1515
from datetime import datetime
1616
from datetime import timezone
1717

18+
from google.adk.events.event import Event
1819
from google.adk.events.event_actions import EventActions
1920
from google.adk.events.event_actions import EventCompaction
2021
from google.adk.sessions.schemas.v0 import StorageEvent
22+
from google.adk.sessions.schemas.v0 import _LEGACY_ERROR_MSG_MAX_LEN
23+
from google.adk.sessions.schemas.v0 import _TRUNCATION_SUFFIX
24+
from google.adk.sessions.session import Session
2125
from google.genai import types
2226

2327

@@ -48,3 +52,37 @@ def test_storage_event_v0_to_event_rehydrates_compaction_model():
4852
assert isinstance(event.actions.compaction, EventCompaction)
4953
assert event.actions.compaction.start_timestamp == 1.0
5054
assert event.actions.compaction.end_timestamp == 2.0
55+
56+
57+
def test_from_event_truncates_error_message_exceeding_varchar255():
58+
session = Session(app_name="app", user_id="user", id="session_id")
59+
event = Event(
60+
id="event_id",
61+
invocation_id="invocation_id",
62+
author="author",
63+
timestamp=1.0,
64+
error_code="MALFORMED_FUNCTION_CALL",
65+
error_message="x" * 300,
66+
)
67+
68+
storage_event = StorageEvent.from_event(session, event)
69+
70+
assert len(storage_event.error_message) <= _LEGACY_ERROR_MSG_MAX_LEN
71+
assert storage_event.error_message.endswith(_TRUNCATION_SUFFIX)
72+
73+
74+
def test_from_event_preserves_short_error_message():
75+
session = Session(app_name="app", user_id="user", id="session_id")
76+
short_message = "Malformed function call"
77+
event = Event(
78+
id="event_id",
79+
invocation_id="invocation_id",
80+
author="author",
81+
timestamp=1.0,
82+
error_code="MALFORMED_FUNCTION_CALL",
83+
error_message=short_message,
84+
)
85+
86+
storage_event = StorageEvent.from_event(session, event)
87+
88+
assert storage_event.error_message == short_message

0 commit comments

Comments
 (0)