Skip to content

SqliteSessionService turns state integers outside 64 bits into floats聽#7576

Closed

Description

@Somay-kousis

馃敶 Required Information

Describe the Bug:

SqliteSessionService stores an integer outside the signed 64-bit range in session state as a float. A 64-bit hash like 0xFEDCBA9876543210 comes back as 1.8364758544493064e+19 and no longer equals the original.

The state merge (_MERGE_STATE_SQL) rebuilds every key of the row inside SQLite, and SQLite parses such an integer as REAL. So a big int stored earlier also turns into a float when a later turn writes an unrelated key. This is the store adk web and adk api_server use by default (.adk/session.db). The in-memory and database services keep the value exact.

#7426 fixed float rounding in the same SQL and left this out on purpose: "Integers beyond 64 bits are still parsed as REAL by SQLite".

Steps to Reproduce:

  1. Checkout of main
  2. Save the script below as repro.py
  3. Run PYTHONPATH=src python repro.py. No network: the model is a fake that calls the tool named in the user message.

Expected Behavior:

state['fingerprint'] stays 18364758544493064720 (int) on every turn, and check_fingerprint sees a match.

Observed Behavior:

expected                     18364758544493064720 (int)
after remember_fingerprint   state['fingerprint'] = 1.8364758544493064e+19 (float)
after count_visit            state['fingerprint'] = 1.8364758544493064e+19 (float)
after check_fingerprint      state['fingerprint'] = 1.8364758544493064e+19 (float)
check_fingerprint tool saw: {'matches': False, 'value': '1.8364758544493064e+19'}

Environment Details:

  • ADK Library Version (pip show google-adk): 2.12.0, main at f78149a
  • Desktop OS: macOS 26.6.2
  • Python Version (python -V): Python 3.12.13, SQLite 3.53.4

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: a fake BaseLlm, no real model call

馃煛 Optional Information

Root cause:

In _MERGE_STATE_SQL (src/google/adk/sessions/sqlite_session_service.py:59), json_each reports such a value with type = 'integer' but typeof(value) = 'real', and json_group_object writes the REAL. Values nested inside an object or list keep their JSON text, so only top-level keys are hit.

Minimal Reproduction Code:

import asyncio
import os
import tempfile
from typing import AsyncGenerator

from google.adk.agents import LlmAgent
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.adk.runners import Runner
from google.adk.sessions.sqlite_session_service import SqliteSessionService
from google.adk.tools.tool_context import ToolContext
from google.genai import types

FINGERPRINT = 0xFEDCBA9876543210  # an unsigned 64-bit hash, > 2**63 - 1


class FakeLlm(BaseLlm):
  """Calls the tool named in the user message, then answers 'done'."""

  model: str = "fake"

  async def generate_content_async(
      self, llm_request: LlmRequest, stream: bool = False
  ) -> AsyncGenerator[LlmResponse, None]:
    last = llm_request.contents[-1]
    if any(p.function_response for p in last.parts):
      yield LlmResponse(content=types.ModelContent("done"))
      return
    tool = last.parts[0].text
    yield LlmResponse(
        content=types.ModelContent(
            [types.Part.from_function_call(name=tool, args={})]
        )
    )


def remember_fingerprint(tool_context: ToolContext) -> dict:
  """Stores a 64-bit document fingerprint."""
  tool_context.state["fingerprint"] = FINGERPRINT
  return {"stored": True}


def count_visit(tool_context: ToolContext) -> dict:
  """Unrelated state write."""
  tool_context.state["visits"] = tool_context.state.get("visits", 0) + 1
  return {"ok": True}


def check_fingerprint(tool_context: ToolContext) -> dict:
  """Compares the stored fingerprint."""
  value = tool_context.state["fingerprint"]
  return {"matches": value == FINGERPRINT, "value": repr(value)}


async def main():
  db = os.path.join(tempfile.mkdtemp(), "session.db")
  agent = LlmAgent(
      name="a",
      model=FakeLlm(),
      tools=[remember_fingerprint, count_visit, check_fingerprint],
  )
  runner = Runner(
      app_name="app", agent=agent, session_service=SqliteSessionService(db)
  )
  s = await runner.session_service.create_session(app_name="app", user_id="u")

  async def turn(tool_name):
    async for _ in runner.run_async(
        user_id="u", session_id=s.id, new_message=types.UserContent(tool_name)
    ):
      pass
    got = await runner.session_service.get_session(
        app_name="app", user_id="u", session_id=s.id
    )
    v = got.state.get("fingerprint")
    print(f"after {tool_name:<22} state['fingerprint'] = {v!r} ({type(v).__name__})")

  print(f"expected                     {FINGERPRINT!r} (int)")
  await turn("remember_fingerprint")
  await turn("count_visit")
  await turn("check_fingerprint")
  got = await runner.session_service.get_session(
      app_name="app", user_id="u", session_id=s.id
  )
  resp = [
      p.function_response.response
      for e in got.events
      for p in (e.content.parts if e.content else [])
      if p.function_response and p.function_response.name == "check_fingerprint"
  ]
  print("check_fingerprint tool saw:", resp[-1])


asyncio.run(main())

Activity

  1. Somay-kousis commented on Oct 17, 2026

    @Somay-kousis
    ContributorAuthor

    For triage: #7577 (opened 2026-10-16) fixes this, and its f094042 adds the guard BichengWang asked for so merging ordinary state stays linear, with a scaling test. #7586 covers the same fix.

  2. added
    services[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc
    on Oct 19, 2026
  3. added a commit that references this issue on Oct 20, 2026
    db432db
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

services[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions