Add app-server APIs for stored thread attachments (#44564)

## What changed

- Add `thread/attachment/add`, `thread/attachment/list`, and `thread/attachment/remove` to manage durable resource references without loading the owning thread or changing conversation history.
- Identify attachments by thread, `attachmentType`, and `identityKey`. Repeated adds return the existing attachment; repeated removals succeed without emitting another update. Listing supports cursor pagination.
- Broadcast `thread/attachment/updated` after creation or deletion, after responding to the requester. Serialize attachment mutations with thread lifecycle operations and reject stores that do not support attachments.
- Update protocol schemas, TypeScript and Python bindings, documentation, and TUI notification handling so attachment updates do not add untracked threads to the agent picker.

## Testing

Add coverage for unloaded threads, pagination, idempotent addition and removal, reattachment, invalid inputs, unsupported stores, response-before-notification ordering across clients, and TUI notification routing.

GitOrigin-RevId: 0d29cf903afd4e931a70a9cf96300475213b34aa
This commit is contained in:
joeytrasatti-openai
2026-09-10 15:41:01 +00:00
committed by copyberry
parent 94697375cb
commit 3319d9b296
47 changed files with 2341 additions and 6 deletions

View File

@@ -54,6 +54,7 @@ from .v2_all import SkillsChangedNotification
from .v2_all import StrictReviewRequiredNotification
from .v2_all import TerminalInteractionNotification
from .v2_all import ThreadArchivedNotification
from .v2_all import ThreadAttachmentUpdatedNotification
from .v2_all import ThreadClosedNotification
from .v2_all import ThreadDeletedNotification
from .v2_all import ThreadGoalClearedNotification
@@ -135,6 +136,7 @@ KnownNotificationPayload: TypeAlias = (
| StrictReviewRequiredNotification
| TerminalInteractionNotification
| ThreadArchivedNotification
| ThreadAttachmentUpdatedNotification
| ThreadClosedNotification
| ThreadDeletedNotification
| ThreadGoalClearedNotification
@@ -216,6 +218,7 @@ NOTIFICATION_MODELS: dict[str, type[KnownNotificationPayload]] = {
"serverRequest/resolved": ServerRequestResolvedNotification,
"skills/changed": SkillsChangedNotification,
"thread/archived": ThreadArchivedNotification,
"thread/attachment/updated": ThreadAttachmentUpdatedNotification,
"thread/closed": ThreadClosedNotification,
"thread/compacted": ContextCompactedNotification,
"thread/deleted": ThreadDeletedNotification,

View File

@@ -5034,6 +5034,89 @@ class ThreadArchivedNotification(BaseModel):
thread_id: Annotated[str, Field(alias="threadId")]
class ThreadAttachment(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
attachment_type: Annotated[str, Field(alias="attachmentType")]
created_at: Annotated[int, Field(alias="createdAt")]
id: str
identity_key: Annotated[str, Field(alias="identityKey")]
payload: Any
class ThreadAttachmentAddOutcome(Enum):
created = "created"
existing = "existing"
class ThreadAttachmentAddParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
attachment_type: Annotated[str, Field(alias="attachmentType")]
identity_key: Annotated[str, Field(alias="identityKey")]
payload: Any
thread_id: Annotated[str, Field(alias="threadId")]
class ThreadAttachmentAddResponse(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
attachment: ThreadAttachment
outcome: ThreadAttachmentAddOutcome
class ThreadAttachmentListParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
cursor: str | None = None
limit: Annotated[int | None, Field(ge=0)] = None
thread_id: Annotated[str, Field(alias="threadId")]
class ThreadAttachmentListResponse(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
data: list[ThreadAttachment]
next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None
class ThreadAttachmentOperation(Enum):
created = "created"
deleted = "deleted"
class ThreadAttachmentRemoveParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
attachment_type: Annotated[str, Field(alias="attachmentType")]
identity_key: Annotated[str, Field(alias="identityKey")]
thread_id: Annotated[str, Field(alias="threadId")]
class ThreadAttachmentRemoveResponse(BaseModel):
pass
model_config = ConfigDict(
populate_by_name=True,
)
class ThreadAttachmentUpdatedNotification(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
attachment_id: Annotated[str, Field(alias="attachmentId")]
attachment_type: Annotated[str, Field(alias="attachmentType")]
identity_key: Annotated[str, Field(alias="identityKey")]
operation: ThreadAttachmentOperation
thread_id: Annotated[str, Field(alias="threadId")]
class ThreadClosedNotification(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -6638,6 +6721,39 @@ class ThreadMetadataUpdateRequest(BaseModel):
params: ThreadMetadataUpdateParams
class ThreadAttachmentAddRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
id: RequestId
method: Annotated[
Literal["thread/attachment/add"], Field(title="Thread/attachment/addRequestMethod")
]
params: ThreadAttachmentAddParams
class ThreadAttachmentListRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
id: RequestId
method: Annotated[
Literal["thread/attachment/list"], Field(title="Thread/attachment/listRequestMethod")
]
params: ThreadAttachmentListParams
class ThreadAttachmentRemoveRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
id: RequestId
method: Annotated[
Literal["thread/attachment/remove"], Field(title="Thread/attachment/removeRequestMethod")
]
params: ThreadAttachmentRemoveParams
class ThreadSectionMoveRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -8776,6 +8892,24 @@ class ThreadNameUpdatedServerNotification(BaseModel):
params: ThreadNameUpdatedNotification
class ThreadAttachmentUpdatedServerNotification(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/attachment/updated"],
Field(title="Thread/attachment/updatedNotificationMethod"),
]
params: ThreadAttachmentUpdatedNotification
class ThreadGoalClearedServerNotification(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -11957,6 +12091,9 @@ class ClientRequest(
| ThreadGoalGetRequest
| ThreadGoalClearRequest
| ThreadMetadataUpdateRequest
| ThreadAttachmentAddRequest
| ThreadAttachmentListRequest
| ThreadAttachmentRemoveRequest
| ThreadSectionMoveRequest
| ThreadUnarchiveRequest
| ThreadCompactStartRequest
@@ -12062,6 +12199,9 @@ class ClientRequest(
| ThreadGoalGetRequest
| ThreadGoalClearRequest
| ThreadMetadataUpdateRequest
| ThreadAttachmentAddRequest
| ThreadAttachmentListRequest
| ThreadAttachmentRemoveRequest
| ThreadSectionMoveRequest
| ThreadUnarchiveRequest
| ThreadCompactStartRequest
@@ -12336,6 +12476,7 @@ class ServerNotification(
| ThreadRevertedServerNotification
| SkillsChangedServerNotification
| ThreadNameUpdatedServerNotification
| ThreadAttachmentUpdatedServerNotification
| ThreadGoalUpdatedServerNotification
| ThreadGoalClearedServerNotification
| ThreadQueueChangedServerNotification
@@ -12423,6 +12564,7 @@ class ServerNotification(
| ThreadRevertedServerNotification
| SkillsChangedServerNotification
| ThreadNameUpdatedServerNotification
| ThreadAttachmentUpdatedServerNotification
| ThreadGoalUpdatedServerNotification
| ThreadGoalClearedServerNotification
| ThreadQueueChangedServerNotification