fix(sdks): keep session activity cache current

This commit is contained in:
steven-ji 2026-08-23 10:32:18 +08:00
parent ba71600d6f
commit 90e05f67e1
6 changed files with 122 additions and 9 deletions

View File

@ -1256,10 +1256,12 @@ class SessionAio(AsyncMetadataConfigMixin):
routes.messages(self._session.workspace_id, self._session.id),
body={"messages": messages_data},
)
return [
created_messages = [
Message.from_api_response(MessageResponse.model_validate(msg))
for msg in data
]
self._session._update_last_message_at_from_messages(created_messages)
return created_messages
async def messages(
self,
@ -1559,10 +1561,12 @@ class SessionAio(AsyncMetadataConfigMixin):
data=data_dict,
)
return [
created_messages = [
Message.from_api_response(MessageResponse.model_validate(msg))
for msg in response
]
self._session._update_last_message_at_from_messages(created_messages)
return created_messages
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
async def representation(

View File

@ -127,6 +127,17 @@ class Session(SessionBase, MetadataConfigMixin):
self._last_message_at = session.last_message_at
self._is_active = session.is_active
def _update_last_message_at_from_messages(
self, messages: Sequence[Message]
) -> None:
"""Advance the cached activity timestamp from locally written messages."""
if not messages:
return
newest_message_at = max(message.created_at for message in messages)
if self._last_message_at is None or newest_message_at > self._last_message_at:
self._last_message_at = newest_message_at
def get_metadata(self) -> dict[str, object]:
"""
Get metadata from the server and update the cache.
@ -451,10 +462,12 @@ class Session(SessionBase, MetadataConfigMixin):
routes.messages(self.workspace_id, self.id),
body={"messages": messages_data},
)
return [
created_messages = [
Message.from_api_response(MessageResponse.model_validate(msg))
for msg in data
]
self._update_last_message_at_from_messages(created_messages)
return created_messages
@validate_call
def messages(
@ -906,10 +919,12 @@ class Session(SessionBase, MetadataConfigMixin):
data=data_dict,
)
return [
created_messages = [
Message.from_api_response(MessageResponse.model_validate(msg))
for msg in response
]
self._update_last_message_at_from_messages(created_messages)
return created_messages
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def representation(

View File

@ -222,7 +222,7 @@ describe('Peer', () => {
const sessionIds = sessions.items.map((s) => s.id)
expect(sessionIds).toContain('peer-sessions-test')
const returned = sessions.items.find((item) => item.id === session.id)
expect(returned?.lastMessageAt).toBeDefined()
expect(typeof returned?.lastMessageAt).toBe('string')
})
test('sessions returns empty for peer in no sessions', async () => {

View File

@ -126,7 +126,7 @@ export interface SessionResponse {
metadata: Record<string, unknown>
configuration: SessionConfigApi
created_at: string
last_message_at?: string | null
last_message_at: string | null
}
export interface SessionCreateParams {

View File

@ -48,6 +48,7 @@ async def test_session_upload_file(
assert text_content in messages[0].content
assert messages[0].peer_id == user.id
assert messages[0].session_id == session.id
assert session.last_message_at == max(message.created_at for message in messages)
@pytest.mark.asyncio

View File

@ -1,15 +1,104 @@
import datetime
from types import SimpleNamespace
from typing import cast
from unittest.mock import AsyncMock, MagicMock
import pytest
from sdks.python.src.honcho.api_types import QueueStatusResponse
from sdks.python.src.honcho.api_types import MessageCreateParams, QueueStatusResponse
from sdks.python.src.honcho.client import Honcho
from sdks.python.src.honcho.message import Message
from sdks.python.src.honcho.peer import Peer
from sdks.python.src.honcho.session import Session, SessionPeerConfig
def _message_response(
message_id: str, created_at: datetime.datetime
) -> dict[str, object]:
"""Build a complete message response fixture for SDK boundary tests."""
return {
"id": message_id,
"content": message_id,
"peer_id": "peer-1",
"session_id": "session-1",
"workspace_id": "workspace-1",
"metadata": {},
"created_at": created_at,
"token_count": 1,
}
def _session_write_honcho() -> (
tuple[SimpleNamespace, datetime.datetime, datetime.datetime]
):
"""Build sync and async HTTP stubs for local session write tests."""
newest_time = datetime.datetime(2026, 1, 10, 12, 0, tzinfo=datetime.timezone.utc)
older_time = datetime.datetime(2026, 1, 5, 12, 0, tzinfo=datetime.timezone.utc)
older_response = [_message_response("backdated activity", older_time)]
newest_response = [_message_response("newest activity", newest_time)]
sync_http = SimpleNamespace(
post=MagicMock(side_effect=[older_response, older_response]),
upload=MagicMock(side_effect=[newest_response, older_response]),
)
async_http = SimpleNamespace(
post=AsyncMock(side_effect=[older_response, older_response]),
upload=AsyncMock(side_effect=[newest_response, older_response]),
)
honcho = SimpleNamespace(
workspace_id="workspace-1",
_http=sync_http,
_async_http_client=async_http,
_ensure_workspace=MagicMock(return_value=None),
_ensure_workspace_async=AsyncMock(return_value=None),
)
return honcho, older_time, newest_time
def test_session_sync_writes_update_last_message_at_monotonically() -> None:
"""Sync message and file writes keep the newest cached activity timestamp."""
honcho, older_time, newest_time = _session_write_honcho()
session = Session("session-1", honcho)
session.add_messages(MessageCreateParams(content="activity", peer_id="peer-1"))
assert session.last_message_at == older_time
session.upload_file(("activity.txt", b"activity", "text/plain"), peer="peer-1")
assert session.last_message_at == newest_time
session.add_messages(MessageCreateParams(content="activity", peer_id="peer-1"))
assert session.last_message_at == newest_time
session.upload_file(("activity.txt", b"activity", "text/plain"), peer="peer-1")
assert session.last_message_at == newest_time
@pytest.mark.asyncio
async def test_session_async_writes_update_last_message_at_monotonically() -> None:
"""Async message and file writes keep the newest cached activity timestamp."""
honcho, older_time, newest_time = _session_write_honcho()
session = Session("session-1", honcho)
await session.aio.add_messages(
MessageCreateParams(content="activity", peer_id="peer-1")
)
assert session.last_message_at == older_time
await session.aio.upload_file(
("activity.txt", b"activity", "text/plain"), peer="peer-1"
)
assert session.last_message_at == newest_time
await session.aio.add_messages(
MessageCreateParams(content="activity", peer_id="peer-1")
)
assert session.last_message_at == newest_time
await session.aio.upload_file(
("activity.txt", b"activity", "text/plain"), peer="peer-1"
)
assert session.last_message_at == newest_time
@pytest.mark.asyncio
async def test_session_metadata(client_fixture: tuple[Honcho, str]):
"""
@ -729,12 +818,14 @@ async def test_session_clone(client_fixture: tuple[Honcho, str]):
cloned = await session.aio.clone()
assert isinstance(cloned, Session)
assert cloned.id != session.id # Should have a different ID
assert cloned.last_message_at is not None
# Verify cloned session has the same messages
cloned_messages_page = await cloned.aio.messages()
cloned_messages = cloned_messages_page.items
assert len(cloned_messages) == 2
assert cloned.last_message_at == max(
message.created_at for message in cloned_messages
)
# Verify original session still has messages
original_messages_page = await session.aio.messages()
@ -758,12 +849,14 @@ async def test_session_clone(client_fixture: tuple[Honcho, str]):
cloned = session.clone()
assert isinstance(cloned, Session)
assert cloned.id != session.id # Should have a different ID
assert cloned.last_message_at is not None
# Verify cloned session has the same messages
cloned_messages_page = cloned.messages()
cloned_messages = list(cloned_messages_page)
assert len(cloned_messages) == 2
assert cloned.last_message_at == max(
message.created_at for message in cloned_messages
)
# Verify original session still has messages
original_messages_page = session.messages()