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 authored and copyberry committed 2026-09-10 15:52:09 +00:00
1 parent 94697375cb
commit 3319d9b296
47 files changed
+2341 -6

No files matched your search

@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Parameters for creating or locating an attachment on its owning thread.",
"properties": {
"attachmentType": {
"type": "string"
},
"identityKey": {
"type": "string"
},
"payload": true,
"threadId": {
"type": "string"
}
},
"required": [
"attachmentType",
"identityKey",
"payload",
"threadId"
],
"title": "ThreadAttachmentAddParams",
"type": "object"
}
@@ -0,0 +1,55 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ThreadAttachment": {
"description": "An independently persisted attachment associated with a thread.",
"properties": {
"attachmentType": {
"type": "string"
},
"createdAt": {
"format": "int64",
"type": "integer"
},
"id": {
"type": "string"
},
"identityKey": {
"type": "string"
},
"payload": true
},
"required": [
"attachmentType",
"createdAt",
"id",
"identityKey",
"payload"
],
"type": "object"
},
"ThreadAttachmentAddOutcome": {
"description": "Result of attempting to associate an attachment with a thread.",
"enum": [
"created",
"existing"
],
"type": "string"
}
},
"description": "The created or existing attachment.",
"properties": {
"attachment": {
"$ref": "#/definitions/ThreadAttachment"
},
"outcome": {
"$ref": "#/definitions/ThreadAttachmentAddOutcome"
}
},
"required": [
"attachment",
"outcome"
],
"title": "ThreadAttachmentAddResponse",
"type": "object"
}
@@ -0,0 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Parameters for listing attachments from one thread.",
"properties": {
"cursor": {
"type": [
"string",
"null"
]
},
"limit": {
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadAttachmentListParams",
"type": "object"
}
@@ -0,0 +1,52 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ThreadAttachment": {
"description": "An independently persisted attachment associated with a thread.",
"properties": {
"attachmentType": {
"type": "string"
},
"createdAt": {
"format": "int64",
"type": "integer"
},
"id": {
"type": "string"
},
"identityKey": {
"type": "string"
},
"payload": true
},
"required": [
"attachmentType",
"createdAt",
"id",
"identityKey",
"payload"
],
"type": "object"
}
},
"description": "One page of attachments associated with the requested thread.",
"properties": {
"data": {
"items": {
"$ref": "#/definitions/ThreadAttachment"
},
"type": "array"
},
"nextCursor": {
"type": [
"string",
"null"
]
}
},
"required": [
"data"
],
"title": "ThreadAttachmentListResponse",
"type": "object"
}
@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Parameters for deleting an attachment by its stable thread-local identity.",
"properties": {
"attachmentType": {
"type": "string"
},
"identityKey": {
"type": "string"
},
"threadId": {
"type": "string"
}
},
"required": [
"attachmentType",
"identityKey",
"threadId"
],
"title": "ThreadAttachmentRemoveParams",
"type": "object"
}
@@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Successful deletion does not return additional attachment data.",
"title": "ThreadAttachmentRemoveResponse",
"type": "object"
}
@@ -0,0 +1,40 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ThreadAttachmentOperation": {
"description": "The persisted attachment change represented by a notification.",
"enum": [
"created",
"deleted"
],
"type": "string"
}
},
"description": "Notification published after a thread attachment is created or deleted.",
"properties": {
"attachmentId": {
"type": "string"
},
"attachmentType": {
"type": "string"
},
"identityKey": {
"type": "string"
},
"operation": {
"$ref": "#/definitions/ThreadAttachmentOperation"
},
"threadId": {
"type": "string"
}
},
"required": [
"attachmentId",
"attachmentType",
"identityKey",
"operation",
"threadId"
],
"title": "ThreadAttachmentUpdatedNotification",
"type": "object"
}