Notify subscribers when thread read state changes (#52384)

## What changed

Add the experimental `thread/readState/changed` notification with `threadId` and the committed `readState` receipt. Send it to existing thread subscribers when terminal activity updates read state or a revert clears a removed unread position. Emit terminal read-state updates before `turn/completed` so completion remains the last notification for that result.

Expose the notification in generated JSON schemas, TypeScript types, and Python SDK models. Document experimental capability gating, subscription lifetime, notification opt-out, and refreshing receipts when concurrent responses disagree.

## Testing

Extend the read-state integration test to assert that the notification receipt matches the receipt returned by `thread/read` after terminal activity.

GitOrigin-RevId: 28935352a881b7a5e2e7f629173c3b2061bd7941
This commit is contained in:
Brent Traut
2026-10-09 02:59:21 +00:00
committed by copyberry
parent 5b3c1522f8
commit 82883da25e
22 changed files with 382 additions and 11 deletions
@@ -64,6 +64,7 @@ from .v2_all import ThreadNameUpdatedNotification
from .v2_all import ThreadPredictionUpdatedNotification
from .v2_all import ThreadProjectUpdatedNotification
from .v2_all import ThreadQueueChangedNotification
from .v2_all import ThreadReadStateChangedNotification
from .v2_all import ThreadRealtimeClosedNotification
from .v2_all import ThreadRealtimeErrorNotification
from .v2_all import ThreadRealtimeItemAddedNotification
@@ -148,6 +149,7 @@ KnownNotificationPayload: TypeAlias = (
| ThreadPredictionUpdatedNotification
| ThreadProjectUpdatedNotification
| ThreadQueueChangedNotification
| ThreadReadStateChangedNotification
| ThreadRealtimeClosedNotification
| ThreadRealtimeErrorNotification
| ThreadRealtimeItemAddedNotification
@@ -235,6 +237,7 @@ NOTIFICATION_MODELS: dict[str, type[KnownNotificationPayload]] = {
"thread/prediction/updated": ThreadPredictionUpdatedNotification,
"thread/project/updated": ThreadProjectUpdatedNotification,
"thread/queue/changed": ThreadQueueChangedNotification,
"thread/readState/changed": ThreadReadStateChangedNotification,
"thread/realtime/closed": ThreadRealtimeClosedNotification,
"thread/realtime/error": ThreadRealtimeErrorNotification,
"thread/realtime/item/completed": ThreadRealtimeItemCompletedNotification,
@@ -10249,6 +10249,14 @@ class ThreadReadState(BaseModel):
revision: str
class ThreadReadStateChangedNotification(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
read_state: Annotated[ThreadReadState, Field(alias="readState")]
thread_id: Annotated[str, Field(alias="threadId")]
class TranscriptSegmentThreadRealtimeItem(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -11317,6 +11325,24 @@ class ErrorServerNotification(BaseModel):
params: ErrorNotification
class ThreadReadStateChangedServerNotification(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
emitted_at_ms: Annotated[
int | None,
Field(
alias="emittedAtMs",
description="Unix timestamp (in milliseconds) when app-server emitted this notification.",
),
] = None
method: Annotated[
Literal["thread/readState/changed"],
Field(title="Thread/readState/changedNotificationMethod"),
]
params: ThreadReadStateChangedNotification
class ThreadGoalUpdatedServerNotification(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -13116,6 +13142,7 @@ class ServerNotification(
ErrorServerNotification
| ThreadStartedServerNotification
| ThreadStatusChangedServerNotification
| ThreadReadStateChangedServerNotification
| ThreadArchivedServerNotification
| ThreadDeletedServerNotification
| ThreadUnarchivedServerNotification
@@ -13206,6 +13233,7 @@ class ServerNotification(
ErrorServerNotification
| ThreadStartedServerNotification
| ThreadStatusChangedServerNotification
| ThreadReadStateChangedServerNotification
| ThreadArchivedServerNotification
| ThreadDeletedServerNotification
| ThreadUnarchivedServerNotification