Expose experimental durable thread read state in the app server (#52350)

## What changed

- Add experimental `readState` to `thread/read` and a thread-ID-keyed `readStates` map to `thread/list` for eligible durable local user threads. Each receipt contains `firstUnread` and an opaque `revision`.
- Publish new visible terminal results before `turn/completed`, after flushing the rollout. Reading snapshots leaves activity unread; replayed history does not publish new activity.
- Clear unread positions removed by a paginated thread revert while preserving explicit thread-start marks and concurrent receipt updates.

## Testing

Add an integration test covering completed-turn receipts, repeated reads without acknowledgement, matching list receipts, and revert cleanup with a changed revision.

GitOrigin-RevId: e7f6ddfbb1fc2fa95d1ef2876d7f0f7b9f99ff65
This commit is contained in:
Brent Traut
2026-10-09 01:22:30 +00:00
committed by copyberry
parent b6e8a2e207
commit 0ada5d8806
29 changed files with 711 additions and 18 deletions
@@ -6297,6 +6297,28 @@ class ThreadUnarchivedNotification(BaseModel):
thread_id: Annotated[str, Field(alias="threadId")]
class ThreadStartThreadUnreadPosition(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
type: Annotated[Literal["threadStart"], Field(title="ThreadStartThreadUnreadPositionType")]
class TurnThreadUnreadPosition(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
turn_id: Annotated[str, Field(alias="turnId")]
type: Annotated[Literal["turn"], Field(title="TurnThreadUnreadPositionType")]
class ThreadUnreadPosition(RootModel[ThreadStartThreadUnreadPosition | TurnThreadUnreadPosition]):
model_config = ConfigDict(
populate_by_name=True,
)
root: ThreadStartThreadUnreadPosition | TurnThreadUnreadPosition
class ThreadUnsubscribeParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -10202,6 +10224,14 @@ class ThreadListParams(BaseModel):
] = None
class ThreadReadState(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
first_unread: Annotated[ThreadUnreadPosition | None, Field(alias="firstUnread")] = None
revision: str
class TranscriptSegmentThreadRealtimeItem(BaseModel):
model_config = ConfigDict(
populate_by_name=True,