mirror of
https://github.com/openai/codex.git
synced 2026-11-07 16:35:32 +00:00
Add persisted sections for organizing threads (#35722)
## What changed - Replace the `isPinned` thread metadata and filters with an optional persisted `section` and `sectionId`. - Add the paginated `threadSection/list` app-server method so clients can discover sections even when they contain no threads. - Seed a stable `Pinned` section, validate section assignments, and support filtering for a specific section or for unsectioned threads. ## Testing - Cover section protocol serialization, listing and pagination, metadata updates, filtering, persistence, migration compatibility, and operation without SQLite state. GitOrigin-RevId: 7972b5471d29317b9387bfd90aa9f573f691ad4c
This commit is contained in:
1 parent
9291a123fb
commit
85c6da1c79
85 files changed
+1826
-419
No files matched your search
@@ -204,7 +204,7 @@ fn sample_thread_with_metadata(
|
|||||||
parent_thread_id,
|
parent_thread_id,
|
||||||
preview: "first prompt".to_string(),
|
preview: "first prompt".to_string(),
|
||||||
ephemeral,
|
ephemeral,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "openai".to_string(),
|
model_provider: "openai".to_string(),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
|
|||||||
@@ -479,7 +479,7 @@ fn sample_thread(thread_id: &str) -> Thread {
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: "first prompt".to_string(),
|
preview: "first prompt".to_string(),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "openai".to_string(),
|
model_provider: "openai".to_string(),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
|
|||||||
+56
-10
@@ -4154,13 +4154,6 @@
|
|||||||
],
|
],
|
||||||
"description": "Optional cwd filter or filters; when set, only threads whose session cwd exactly matches one of these paths are returned."
|
"description": "Optional cwd filter or filters; when set, only threads whose session cwd exactly matches one of these paths are returned."
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"description": "Optional pinned filter; when set, only threads matching this value are returned.",
|
|
||||||
"type": [
|
|
||||||
"boolean",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"limit": {
|
"limit": {
|
||||||
"description": "Optional page size; defaults to a reasonable server-side value.",
|
"description": "Optional page size; defaults to a reasonable server-side value.",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -4187,6 +4180,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"sectionId": {
|
||||||
|
"description": "Omit to include every section, set to `null` for unsectioned threads, or provide a section ID to return only threads in that section.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"sortDirection": {
|
"sortDirection": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -4293,10 +4293,10 @@
|
|||||||
],
|
],
|
||||||
"description": "Patch the stored Git metadata for this thread. Omit a field to leave it unchanged, set it to `null` to clear it, or provide a string to replace the stored value."
|
"description": "Patch the stored Git metadata for this thread. Omit a field to leave it unchanged, set it to `null` to clear it, or provide a string to replace the stored value."
|
||||||
},
|
},
|
||||||
"isPinned": {
|
"sectionId": {
|
||||||
"description": "Patch whether this thread is pinned. Omit to leave the stored value unchanged.",
|
"description": "Omit to leave the section unchanged, set to `null` to clear it, or provide a section ID.",
|
||||||
"type": [
|
"type": [
|
||||||
"boolean",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -4572,6 +4572,28 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ThreadSectionListParams": {
|
||||||
|
"description": "Parameters for listing independently persisted thread sections.",
|
||||||
|
"properties": {
|
||||||
|
"cursor": {
|
||||||
|
"description": "Opaque pagination cursor returned by a previous call.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"description": "Maximum number of sections to return.",
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0,
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSetNameParams": {
|
"ThreadSetNameParams": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
@@ -5634,6 +5656,30 @@
|
|||||||
"title": "Thread/listRequest",
|
"title": "Thread/listRequest",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/definitions/RequestId"
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"enum": [
|
||||||
|
"threadSection/list"
|
||||||
|
],
|
||||||
|
"title": "ThreadSection/listRequestMethod",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"$ref": "#/definitions/ThreadSectionListParams"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"method",
|
||||||
|
"params"
|
||||||
|
],
|
||||||
|
"title": "ThreadSection/listRequest",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
|
|||||||
@@ -3758,11 +3758,6 @@
|
|||||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Whether the thread has been pinned by the user.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"modelProvider": {
|
"modelProvider": {
|
||||||
"description": "Model provider used for this thread (for example, 'openai').",
|
"description": "Model provider used for this thread (for example, 'openai').",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -3800,6 +3795,18 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "The independently persisted section selected for this thread, if any."
|
||||||
|
},
|
||||||
"sessionId": {
|
"sessionId": {
|
||||||
"description": "Session id shared by threads that belong to the same session tree.",
|
"description": "Session id shared by threads that belong to the same session tree.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -4969,6 +4976,24 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSettings": {
|
"ThreadSettings": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"activePermissionProfile": {
|
"activePermissionProfile": {
|
||||||
|
|||||||
+112
-15
@@ -636,6 +636,30 @@
|
|||||||
"title": "Thread/listRequest",
|
"title": "Thread/listRequest",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/definitions/v2/RequestId"
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"enum": [
|
||||||
|
"threadSection/list"
|
||||||
|
],
|
||||||
|
"title": "ThreadSection/listRequestMethod",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"$ref": "#/definitions/v2/ThreadSectionListParams"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"method",
|
||||||
|
"params"
|
||||||
|
],
|
||||||
|
"title": "ThreadSection/listRequest",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -18089,11 +18113,6 @@
|
|||||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Whether the thread has been pinned by the user.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"modelProvider": {
|
"modelProvider": {
|
||||||
"description": "Model provider used for this thread (for example, 'openai').",
|
"description": "Model provider used for this thread (for example, 'openai').",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -18131,6 +18150,18 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/v2/ThreadSection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "The independently persisted section selected for this thread, if any."
|
||||||
|
},
|
||||||
"sessionId": {
|
"sessionId": {
|
||||||
"description": "Session id shared by threads that belong to the same session tree.",
|
"description": "Session id shared by threads that belong to the same session tree.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -19554,13 +19585,6 @@
|
|||||||
],
|
],
|
||||||
"description": "Optional cwd filter or filters; when set, only threads whose session cwd exactly matches one of these paths are returned."
|
"description": "Optional cwd filter or filters; when set, only threads whose session cwd exactly matches one of these paths are returned."
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"description": "Optional pinned filter; when set, only threads matching this value are returned.",
|
|
||||||
"type": [
|
|
||||||
"boolean",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"limit": {
|
"limit": {
|
||||||
"description": "Optional page size; defaults to a reasonable server-side value.",
|
"description": "Optional page size; defaults to a reasonable server-side value.",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -19587,6 +19611,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"sectionId": {
|
||||||
|
"description": "Omit to include every section, set to `null` for unsectioned threads, or provide a section ID to return only threads in that section.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"sortDirection": {
|
"sortDirection": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -19751,10 +19782,10 @@
|
|||||||
],
|
],
|
||||||
"description": "Patch the stored Git metadata for this thread. Omit a field to leave it unchanged, set it to `null` to clear it, or provide a string to replace the stored value."
|
"description": "Patch the stored Git metadata for this thread. Omit a field to leave it unchanged, set it to `null` to clear it, or provide a string to replace the stored value."
|
||||||
},
|
},
|
||||||
"isPinned": {
|
"sectionId": {
|
||||||
"description": "Patch whether this thread is pinned. Omit to leave the stored value unchanged.",
|
"description": "Omit to leave the section unchanged, set to `null` to clear it, or provide a section ID.",
|
||||||
"type": [
|
"type": [
|
||||||
"boolean",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -20346,6 +20377,72 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ThreadSectionListParams": {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"description": "Parameters for listing independently persisted thread sections.",
|
||||||
|
"properties": {
|
||||||
|
"cursor": {
|
||||||
|
"description": "Opaque pagination cursor returned by a previous call.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"description": "Maximum number of sections to return.",
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0,
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ThreadSectionListParams",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ThreadSectionListResponse": {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"description": "One page of independently persisted thread sections.",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/v2/ThreadSection"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"nextCursor": {
|
||||||
|
"description": "Opaque cursor for the next page, or `null` when no sections remain.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"data"
|
||||||
|
],
|
||||||
|
"title": "ThreadSectionListResponse",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSetNameParams": {
|
"ThreadSetNameParams": {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
+112
-15
@@ -1786,6 +1786,30 @@
|
|||||||
"title": "Thread/listRequest",
|
"title": "Thread/listRequest",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/definitions/RequestId"
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"enum": [
|
||||||
|
"threadSection/list"
|
||||||
|
],
|
||||||
|
"title": "ThreadSection/listRequestMethod",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"$ref": "#/definitions/ThreadSectionListParams"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"method",
|
||||||
|
"params"
|
||||||
|
],
|
||||||
|
"title": "ThreadSection/listRequest",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -15853,11 +15877,6 @@
|
|||||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Whether the thread has been pinned by the user.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"modelProvider": {
|
"modelProvider": {
|
||||||
"description": "Model provider used for this thread (for example, 'openai').",
|
"description": "Model provider used for this thread (for example, 'openai').",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -15895,6 +15914,18 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "The independently persisted section selected for this thread, if any."
|
||||||
|
},
|
||||||
"sessionId": {
|
"sessionId": {
|
||||||
"description": "Session id shared by threads that belong to the same session tree.",
|
"description": "Session id shared by threads that belong to the same session tree.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -17318,13 +17349,6 @@
|
|||||||
],
|
],
|
||||||
"description": "Optional cwd filter or filters; when set, only threads whose session cwd exactly matches one of these paths are returned."
|
"description": "Optional cwd filter or filters; when set, only threads whose session cwd exactly matches one of these paths are returned."
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"description": "Optional pinned filter; when set, only threads matching this value are returned.",
|
|
||||||
"type": [
|
|
||||||
"boolean",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"limit": {
|
"limit": {
|
||||||
"description": "Optional page size; defaults to a reasonable server-side value.",
|
"description": "Optional page size; defaults to a reasonable server-side value.",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -17351,6 +17375,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"sectionId": {
|
||||||
|
"description": "Omit to include every section, set to `null` for unsectioned threads, or provide a section ID to return only threads in that section.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"sortDirection": {
|
"sortDirection": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -17515,10 +17546,10 @@
|
|||||||
],
|
],
|
||||||
"description": "Patch the stored Git metadata for this thread. Omit a field to leave it unchanged, set it to `null` to clear it, or provide a string to replace the stored value."
|
"description": "Patch the stored Git metadata for this thread. Omit a field to leave it unchanged, set it to `null` to clear it, or provide a string to replace the stored value."
|
||||||
},
|
},
|
||||||
"isPinned": {
|
"sectionId": {
|
||||||
"description": "Patch whether this thread is pinned. Omit to leave the stored value unchanged.",
|
"description": "Omit to leave the section unchanged, set to `null` to clear it, or provide a section ID.",
|
||||||
"type": [
|
"type": [
|
||||||
"boolean",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -18110,6 +18141,72 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ThreadSectionListParams": {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"description": "Parameters for listing independently persisted thread sections.",
|
||||||
|
"properties": {
|
||||||
|
"cursor": {
|
||||||
|
"description": "Opaque pagination cursor returned by a previous call.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"description": "Maximum number of sections to return.",
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0,
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ThreadSectionListParams",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ThreadSectionListResponse": {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"description": "One page of independently persisted thread sections.",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"nextCursor": {
|
||||||
|
"description": "Opaque cursor for the next page, or `null` when no sections remain.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"data"
|
||||||
|
],
|
||||||
|
"title": "ThreadSectionListResponse",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSetNameParams": {
|
"ThreadSetNameParams": {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1110,11 +1110,6 @@
|
|||||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Whether the thread has been pinned by the user.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"modelProvider": {
|
"modelProvider": {
|
||||||
"description": "Model provider used for this thread (for example, 'openai').",
|
"description": "Model provider used for this thread (for example, 'openai').",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1152,6 +1147,18 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "The independently persisted section selected for this thread, if any."
|
||||||
|
},
|
||||||
"sessionId": {
|
"sessionId": {
|
||||||
"description": "Session id shared by threads that belong to the same session tree.",
|
"description": "Session id shared by threads that belong to the same session tree.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -2000,6 +2007,24 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSource": {
|
"ThreadSource": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -71,13 +71,6 @@
|
|||||||
],
|
],
|
||||||
"description": "Optional cwd filter or filters; when set, only threads whose session cwd exactly matches one of these paths are returned."
|
"description": "Optional cwd filter or filters; when set, only threads whose session cwd exactly matches one of these paths are returned."
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"description": "Optional pinned filter; when set, only threads matching this value are returned.",
|
|
||||||
"type": [
|
|
||||||
"boolean",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"limit": {
|
"limit": {
|
||||||
"description": "Optional page size; defaults to a reasonable server-side value.",
|
"description": "Optional page size; defaults to a reasonable server-side value.",
|
||||||
"format": "uint32",
|
"format": "uint32",
|
||||||
@@ -104,6 +97,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"sectionId": {
|
||||||
|
"description": "Omit to include every section, set to `null` for unsectioned threads, or provide a section ID to return only threads in that section.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"sortDirection": {
|
"sortDirection": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -901,11 +901,6 @@
|
|||||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Whether the thread has been pinned by the user.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"modelProvider": {
|
"modelProvider": {
|
||||||
"description": "Model provider used for this thread (for example, 'openai').",
|
"description": "Model provider used for this thread (for example, 'openai').",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -943,6 +938,18 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "The independently persisted section selected for this thread, if any."
|
||||||
|
},
|
||||||
"sessionId": {
|
"sessionId": {
|
||||||
"description": "Session id shared by threads that belong to the same session tree.",
|
"description": "Session id shared by threads that belong to the same session tree.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1791,6 +1798,24 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSource": {
|
"ThreadSource": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-3
@@ -40,10 +40,10 @@
|
|||||||
],
|
],
|
||||||
"description": "Patch the stored Git metadata for this thread. Omit a field to leave it unchanged, set it to `null` to clear it, or provide a string to replace the stored value."
|
"description": "Patch the stored Git metadata for this thread. Omit a field to leave it unchanged, set it to `null` to clear it, or provide a string to replace the stored value."
|
||||||
},
|
},
|
||||||
"isPinned": {
|
"sectionId": {
|
||||||
"description": "Patch whether this thread is pinned. Omit to leave the stored value unchanged.",
|
"description": "Omit to leave the section unchanged, set to `null` to clear it, or provide a section ID.",
|
||||||
"type": [
|
"type": [
|
||||||
"boolean",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+30
-5
@@ -901,11 +901,6 @@
|
|||||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Whether the thread has been pinned by the user.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"modelProvider": {
|
"modelProvider": {
|
||||||
"description": "Model provider used for this thread (for example, 'openai').",
|
"description": "Model provider used for this thread (for example, 'openai').",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -943,6 +938,18 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "The independently persisted section selected for this thread, if any."
|
||||||
|
},
|
||||||
"sessionId": {
|
"sessionId": {
|
||||||
"description": "Session id shared by threads that belong to the same session tree.",
|
"description": "Session id shared by threads that belong to the same session tree.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1791,6 +1798,24 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSource": {
|
"ThreadSource": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -901,11 +901,6 @@
|
|||||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Whether the thread has been pinned by the user.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"modelProvider": {
|
"modelProvider": {
|
||||||
"description": "Model provider used for this thread (for example, 'openai').",
|
"description": "Model provider used for this thread (for example, 'openai').",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -943,6 +938,18 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "The independently persisted section selected for this thread, if any."
|
||||||
|
},
|
||||||
"sessionId": {
|
"sessionId": {
|
||||||
"description": "Session id shared by threads that belong to the same session tree.",
|
"description": "Session id shared by threads that belong to the same session tree.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1791,6 +1798,24 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSource": {
|
"ThreadSource": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1110,11 +1110,6 @@
|
|||||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Whether the thread has been pinned by the user.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"modelProvider": {
|
"modelProvider": {
|
||||||
"description": "Model provider used for this thread (for example, 'openai').",
|
"description": "Model provider used for this thread (for example, 'openai').",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1152,6 +1147,18 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "The independently persisted section selected for this thread, if any."
|
||||||
|
},
|
||||||
"sessionId": {
|
"sessionId": {
|
||||||
"description": "Session id shared by threads that belong to the same session tree.",
|
"description": "Session id shared by threads that belong to the same session tree.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -2000,6 +2007,24 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSource": {
|
"ThreadSource": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -901,11 +901,6 @@
|
|||||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Whether the thread has been pinned by the user.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"modelProvider": {
|
"modelProvider": {
|
||||||
"description": "Model provider used for this thread (for example, 'openai').",
|
"description": "Model provider used for this thread (for example, 'openai').",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -943,6 +938,18 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "The independently persisted section selected for this thread, if any."
|
||||||
|
},
|
||||||
"sessionId": {
|
"sessionId": {
|
||||||
"description": "Session id shared by threads that belong to the same session tree.",
|
"description": "Session id shared by threads that belong to the same session tree.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1791,6 +1798,24 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSource": {
|
"ThreadSource": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"description": "Parameters for listing independently persisted thread sections.",
|
||||||
|
"properties": {
|
||||||
|
"cursor": {
|
||||||
|
"description": "Opaque pagination cursor returned by a previous call.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"description": "Maximum number of sections to return.",
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0,
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ThreadSectionListParams",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"definitions": {
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "One page of independently persisted thread sections.",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"nextCursor": {
|
||||||
|
"description": "Opaque cursor for the next page, or `null` when no sections remain.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"data"
|
||||||
|
],
|
||||||
|
"title": "ThreadSectionListResponse",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
@@ -1110,11 +1110,6 @@
|
|||||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Whether the thread has been pinned by the user.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"modelProvider": {
|
"modelProvider": {
|
||||||
"description": "Model provider used for this thread (for example, 'openai').",
|
"description": "Model provider used for this thread (for example, 'openai').",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1152,6 +1147,18 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "The independently persisted section selected for this thread, if any."
|
||||||
|
},
|
||||||
"sessionId": {
|
"sessionId": {
|
||||||
"description": "Session id shared by threads that belong to the same session tree.",
|
"description": "Session id shared by threads that belong to the same session tree.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -2000,6 +2007,24 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSource": {
|
"ThreadSource": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
+30
-5
@@ -901,11 +901,6 @@
|
|||||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Whether the thread has been pinned by the user.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"modelProvider": {
|
"modelProvider": {
|
||||||
"description": "Model provider used for this thread (for example, 'openai').",
|
"description": "Model provider used for this thread (for example, 'openai').",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -943,6 +938,18 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "The independently persisted section selected for this thread, if any."
|
||||||
|
},
|
||||||
"sessionId": {
|
"sessionId": {
|
||||||
"description": "Session id shared by threads that belong to the same session tree.",
|
"description": "Session id shared by threads that belong to the same session tree.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1791,6 +1798,24 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSource": {
|
"ThreadSource": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -901,11 +901,6 @@
|
|||||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isPinned": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Whether the thread has been pinned by the user.",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"modelProvider": {
|
"modelProvider": {
|
||||||
"description": "Model provider used for this thread (for example, 'openai').",
|
"description": "Model provider used for this thread (for example, 'openai').",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -943,6 +938,18 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ThreadSection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "The independently persisted section selected for this thread, if any."
|
||||||
|
},
|
||||||
"sessionId": {
|
"sessionId": {
|
||||||
"description": "Session id shared by threads that belong to the same session tree.",
|
"description": "Session id shared by threads that belong to the same session tree.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1791,6 +1798,24 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ThreadSection": {
|
||||||
|
"description": "An independently persisted, user-visible thread section.",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "Opaque UUIDv7 identity that remains stable when the section is renamed.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The current user-visible section name.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadSource": {
|
"ThreadSource": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import type { ThreadMetadataUpdateParams } from "./v2/ThreadMetadataUpdateParams
|
|||||||
import type { ThreadReadParams } from "./v2/ThreadReadParams";
|
import type { ThreadReadParams } from "./v2/ThreadReadParams";
|
||||||
import type { ThreadResumeParams } from "./v2/ThreadResumeParams";
|
import type { ThreadResumeParams } from "./v2/ThreadResumeParams";
|
||||||
import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams";
|
import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams";
|
||||||
|
import type { ThreadSectionListParams } from "./v2/ThreadSectionListParams";
|
||||||
import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams";
|
import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams";
|
||||||
import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams";
|
import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams";
|
||||||
import type { ThreadStartParams } from "./v2/ThreadStartParams";
|
import type { ThreadStartParams } from "./v2/ThreadStartParams";
|
||||||
@@ -91,4 +92,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta
|
|||||||
/**
|
/**
|
||||||
* Request from the client to the server.
|
* Request from the client to the server.
|
||||||
*/
|
*/
|
||||||
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/read", id: RequestId, params: AppsReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "app/installed", id: RequestId, params: AppsInstalledParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mLine truncated
|
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "threadSection/list", id: RequestId, params: ThreadSectionListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/read", id: RequestId, params: AppsReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "app/installed", id: RequestId, params: AppsInstalledParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauLine truncated
|
||||||
+3
-2
@@ -4,6 +4,7 @@
|
|||||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||||
import type { GitInfo } from "./GitInfo";
|
import type { GitInfo } from "./GitInfo";
|
||||||
import type { SessionSource } from "./SessionSource";
|
import type { SessionSource } from "./SessionSource";
|
||||||
|
import type { ThreadSection } from "./ThreadSection";
|
||||||
import type { ThreadSource } from "./ThreadSource";
|
import type { ThreadSource } from "./ThreadSource";
|
||||||
import type { ThreadStatus } from "./ThreadStatus";
|
import type { ThreadStatus } from "./ThreadStatus";
|
||||||
import type { Turn } from "./Turn";
|
import type { Turn } from "./Turn";
|
||||||
@@ -27,9 +28,9 @@ preview: string, /**
|
|||||||
* Whether the thread is ephemeral and should not be materialized on disk.
|
* Whether the thread is ephemeral and should not be materialized on disk.
|
||||||
*/
|
*/
|
||||||
ephemeral: boolean, /**
|
ephemeral: boolean, /**
|
||||||
* Whether the thread has been pinned by the user.
|
* The independently persisted section selected for this thread, if any.
|
||||||
*/
|
*/
|
||||||
isPinned: boolean, /**
|
section: ThreadSection | null, /**
|
||||||
* Model provider used for this thread (for example, 'openai').
|
* Model provider used for this thread (for example, 'openai').
|
||||||
*/
|
*/
|
||||||
modelProvider: string, /**
|
modelProvider: string, /**
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ sourceKinds?: Array<ThreadSourceKind> | null, /**
|
|||||||
* If false or null, only non-archived threads are returned.
|
* If false or null, only non-archived threads are returned.
|
||||||
*/
|
*/
|
||||||
archived?: boolean | null, /**
|
archived?: boolean | null, /**
|
||||||
* Optional pinned filter; when set, only threads matching this value are returned.
|
* Omit to include every section, set to `null` for unsectioned threads,
|
||||||
|
* or provide a section ID to return only threads in that section.
|
||||||
*/
|
*/
|
||||||
isPinned?: boolean | null, /**
|
sectionId?: string | null, /**
|
||||||
* Optional cwd filter or filters; when set, only threads whose session cwd
|
* Optional cwd filter or filters; when set, only threads whose session cwd
|
||||||
* exactly matches one of these paths are returned.
|
* exactly matches one of these paths are returned.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+2
-2
@@ -11,6 +11,6 @@ export type ThreadMetadataUpdateParams = { threadId: string,
|
|||||||
*/
|
*/
|
||||||
gitInfo?: ThreadMetadataGitInfoUpdateParams | null,
|
gitInfo?: ThreadMetadataGitInfoUpdateParams | null,
|
||||||
/**
|
/**
|
||||||
* Patch whether this thread is pinned. Omit to leave the stored value unchanged.
|
* Omit to leave the section unchanged, set to `null` to clear it, or provide a section ID.
|
||||||
*/
|
*/
|
||||||
isPinned?: boolean | null, };
|
sectionId?: string | null, };
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||||
|
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An independently persisted, user-visible thread section.
|
||||||
|
*/
|
||||||
|
export type ThreadSection = {
|
||||||
|
/**
|
||||||
|
* Opaque UUIDv7 identity that remains stable when the section is renamed.
|
||||||
|
*/
|
||||||
|
id: string,
|
||||||
|
/**
|
||||||
|
* The current user-visible section name.
|
||||||
|
*/
|
||||||
|
name: string, };
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||||
|
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for listing independently persisted thread sections.
|
||||||
|
*/
|
||||||
|
export type ThreadSectionListParams = {
|
||||||
|
/**
|
||||||
|
* Opaque pagination cursor returned by a previous call.
|
||||||
|
*/
|
||||||
|
cursor?: string | null,
|
||||||
|
/**
|
||||||
|
* Maximum number of sections to return.
|
||||||
|
*/
|
||||||
|
limit?: number | null, };
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||||
|
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { ThreadSection } from "./ThreadSection";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One page of independently persisted thread sections.
|
||||||
|
*/
|
||||||
|
export type ThreadSectionListResponse = { data: Array<ThreadSection>,
|
||||||
|
/**
|
||||||
|
* Opaque cursor for the next page, or `null` when no sections remain.
|
||||||
|
*/
|
||||||
|
nextCursor: string | null, };
|
||||||
@@ -467,6 +467,9 @@ export type { ThreadResumeResponse } from "./ThreadResumeResponse";
|
|||||||
export type { ThreadRollbackParams } from "./ThreadRollbackParams";
|
export type { ThreadRollbackParams } from "./ThreadRollbackParams";
|
||||||
export type { ThreadRollbackResponse } from "./ThreadRollbackResponse";
|
export type { ThreadRollbackResponse } from "./ThreadRollbackResponse";
|
||||||
export type { ThreadSearchResult } from "./ThreadSearchResult";
|
export type { ThreadSearchResult } from "./ThreadSearchResult";
|
||||||
|
export type { ThreadSection } from "./ThreadSection";
|
||||||
|
export type { ThreadSectionListParams } from "./ThreadSectionListParams";
|
||||||
|
export type { ThreadSectionListResponse } from "./ThreadSectionListResponse";
|
||||||
export type { ThreadSetNameParams } from "./ThreadSetNameParams";
|
export type { ThreadSetNameParams } from "./ThreadSetNameParams";
|
||||||
export type { ThreadSetNameResponse } from "./ThreadSetNameResponse";
|
export type { ThreadSetNameResponse } from "./ThreadSetNameResponse";
|
||||||
export type { ThreadSettings } from "./ThreadSettings";
|
export type { ThreadSettings } from "./ThreadSettings";
|
||||||
|
|||||||
@@ -633,6 +633,11 @@ client_request_definitions! {
|
|||||||
serialization: None,
|
serialization: None,
|
||||||
response: v2::ThreadListResponse,
|
response: v2::ThreadListResponse,
|
||||||
},
|
},
|
||||||
|
ThreadSectionList => "threadSection/list" {
|
||||||
|
params: v2::ThreadSectionListParams,
|
||||||
|
serialization: None,
|
||||||
|
response: v2::ThreadSectionListResponse,
|
||||||
|
},
|
||||||
#[experimental("thread/search")]
|
#[experimental("thread/search")]
|
||||||
ThreadSearch => "thread/search" {
|
ThreadSearch => "thread/search" {
|
||||||
params: v2::ThreadSearchParams,
|
params: v2::ThreadSearchParams,
|
||||||
@@ -2730,7 +2735,7 @@ mod tests {
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: "first prompt".to_string(),
|
preview: "first prompt".to_string(),
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "openai".to_string(),
|
model_provider: "openai".to_string(),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
@@ -2783,7 +2788,7 @@ mod tests {
|
|||||||
"parentThreadId": null,
|
"parentThreadId": null,
|
||||||
"preview": "first prompt",
|
"preview": "first prompt",
|
||||||
"ephemeral": true,
|
"ephemeral": true,
|
||||||
"isPinned": false,
|
"section": null,
|
||||||
"historyMode": "legacy",
|
"historyMode": "legacy",
|
||||||
"modelProvider": "openai",
|
"modelProvider": "openai",
|
||||||
"createdAt": 1,
|
"createdAt": 1,
|
||||||
|
|||||||
@@ -185,7 +185,10 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: String::new(),
|
preview: String::new(),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: true,
|
section: Some(ThreadSection {
|
||||||
|
id: "01984de2-8f74-7c91-a3b2-5c5e937cf318".to_string(),
|
||||||
|
name: "Pinned".to_string(),
|
||||||
|
}),
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "openai".to_string(),
|
model_provider: "openai".to_string(),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
@@ -226,16 +229,22 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let value = serde_json::to_value(&response).expect("serialize thread resume response");
|
let value = serde_json::to_value(&response).expect("serialize thread resume response");
|
||||||
assert_eq!(value["thread"]["isPinned"], json!(true));
|
assert_eq!(
|
||||||
|
value["thread"]["section"],
|
||||||
|
json!({
|
||||||
|
"id": "01984de2-8f74-7c91-a3b2-5c5e937cf318",
|
||||||
|
"name": "Pinned",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
let mut legacy_thread = value["thread"].clone();
|
let mut legacy_thread = value["thread"].clone();
|
||||||
legacy_thread
|
legacy_thread
|
||||||
.as_object_mut()
|
.as_object_mut()
|
||||||
.expect("serialized thread should be an object")
|
.expect("serialized thread should be an object")
|
||||||
.remove("isPinned");
|
.remove("section");
|
||||||
let legacy_thread =
|
let legacy_thread =
|
||||||
serde_json::from_value::<Thread>(legacy_thread).expect("deserialize legacy thread");
|
serde_json::from_value::<Thread>(legacy_thread).expect("deserialize legacy thread");
|
||||||
assert!(!legacy_thread.is_pinned);
|
assert_eq!(legacy_thread.section, None);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value.get("initialTurnsPage"),
|
value.get("initialTurnsPage"),
|
||||||
@@ -369,39 +378,122 @@ fn thread_list_params_accepts_state_db_only_flag() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn thread_list_params_accepts_pinned_filter() {
|
fn thread_list_params_accepts_section_id_filter() {
|
||||||
for is_pinned in [true, false] {
|
for section_id in [
|
||||||
|
"01984de2-8f74-7c91-a3b2-5c5e937cf318",
|
||||||
|
"01984de2-8f74-7c91-a3b2-5c5e937cf319",
|
||||||
|
] {
|
||||||
let params = serde_json::from_value::<ThreadListParams>(json!({
|
let params = serde_json::from_value::<ThreadListParams>(json!({
|
||||||
"isPinned": is_pinned,
|
"sectionId": section_id,
|
||||||
}))
|
}))
|
||||||
.expect("pinned filter should deserialize");
|
.expect("section ID filter should deserialize");
|
||||||
|
|
||||||
assert_eq!(params.is_pinned, Some(is_pinned));
|
assert_eq!(
|
||||||
|
params.section_id.as_ref().map(|section| section.as_deref()),
|
||||||
|
Some(Some(section_id))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(¶ms)
|
||||||
|
.expect("section ID filter should serialize")
|
||||||
|
.get("sectionId"),
|
||||||
|
Some(&json!(section_id))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let params = serde_json::from_value::<ThreadListParams>(json!({
|
||||||
|
"sectionId": null,
|
||||||
|
}))
|
||||||
|
.expect("unsectioned thread filter should deserialize");
|
||||||
|
assert_eq!(params.section_id, Some(None));
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(¶ms)
|
||||||
|
.expect("unsectioned thread filter should serialize")
|
||||||
|
.get("sectionId"),
|
||||||
|
Some(&json!(null))
|
||||||
|
);
|
||||||
|
|
||||||
let params = serde_json::from_value::<ThreadListParams>(json!({}))
|
let params = serde_json::from_value::<ThreadListParams>(json!({}))
|
||||||
.expect("omitted pinned filter should deserialize");
|
.expect("omitted section ID filter should deserialize");
|
||||||
assert_eq!(params.is_pinned, None);
|
assert_eq!(params.section_id, None);
|
||||||
|
assert!(
|
||||||
|
serde_json::to_value(¶ms)
|
||||||
|
.expect("omitted section ID filter should serialize")
|
||||||
|
.get("sectionId")
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn thread_metadata_update_params_accepts_pinned_patch() {
|
fn thread_metadata_update_params_distinguish_section_id_set_clear_and_omission() {
|
||||||
for is_pinned in [true, false] {
|
for section_id in [
|
||||||
|
"01984de2-8f74-7c91-a3b2-5c5e937cf318",
|
||||||
|
"01984de2-8f74-7c91-a3b2-5c5e937cf319",
|
||||||
|
] {
|
||||||
let params = serde_json::from_value::<ThreadMetadataUpdateParams>(json!({
|
let params = serde_json::from_value::<ThreadMetadataUpdateParams>(json!({
|
||||||
"threadId": "thr_123",
|
"threadId": "thr_123",
|
||||||
"isPinned": is_pinned,
|
"sectionId": section_id,
|
||||||
}))
|
}))
|
||||||
.expect("pinned metadata patch should deserialize");
|
.expect("section ID metadata patch should deserialize");
|
||||||
|
|
||||||
assert_eq!(params.is_pinned, Some(is_pinned));
|
assert_eq!(
|
||||||
|
params.section_id.as_ref().map(|section| section.as_deref()),
|
||||||
|
Some(Some(section_id))
|
||||||
|
);
|
||||||
assert_eq!(params.git_info, None);
|
assert_eq!(params.git_info, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let params = serde_json::from_value::<ThreadMetadataUpdateParams>(json!({
|
let params = serde_json::from_value::<ThreadMetadataUpdateParams>(json!({
|
||||||
"threadId": "thr_123",
|
"threadId": "thr_123",
|
||||||
|
"sectionId": null,
|
||||||
}))
|
}))
|
||||||
.expect("omitted pinned metadata patch should deserialize");
|
.expect("cleared section ID metadata patch should deserialize");
|
||||||
assert_eq!(params.is_pinned, None);
|
assert_eq!(params.section_id, Some(None));
|
||||||
|
|
||||||
|
let params = serde_json::from_value::<ThreadMetadataUpdateParams>(json!({
|
||||||
|
"threadId": "thr_123",
|
||||||
|
}))
|
||||||
|
.expect("omitted section ID metadata patch should deserialize");
|
||||||
|
assert_eq!(params.section_id, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn thread_section_list_params_and_response_round_trip() {
|
||||||
|
let params = serde_json::from_value::<ThreadSectionListParams>(json!({
|
||||||
|
"cursor": "section-cursor",
|
||||||
|
"limit": 25,
|
||||||
|
}))
|
||||||
|
.expect("section list parameters should deserialize");
|
||||||
|
assert_eq!(
|
||||||
|
params,
|
||||||
|
ThreadSectionListParams {
|
||||||
|
cursor: Some("section-cursor".to_string()),
|
||||||
|
limit: Some(25),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = ThreadSectionListResponse {
|
||||||
|
data: vec![ThreadSection {
|
||||||
|
id: "01984de2-8f74-7c91-a3b2-5c5e937cf318".to_string(),
|
||||||
|
name: "Pinned".to_string(),
|
||||||
|
}],
|
||||||
|
next_cursor: None,
|
||||||
|
};
|
||||||
|
let value = serde_json::to_value(&response).expect("section list should serialize");
|
||||||
|
assert_eq!(
|
||||||
|
value,
|
||||||
|
json!({
|
||||||
|
"data": [{
|
||||||
|
"id": "01984de2-8f74-7c91-a3b2-5c5e937cf318",
|
||||||
|
"name": "Pinned",
|
||||||
|
}],
|
||||||
|
"nextCursor": null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_value::<ThreadSectionListResponse>(value)
|
||||||
|
.expect("section list should deserialize"),
|
||||||
|
response
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use super::SandboxPolicy;
|
|||||||
use super::Thread;
|
use super::Thread;
|
||||||
use super::ThreadHistoryMode;
|
use super::ThreadHistoryMode;
|
||||||
use super::ThreadItem;
|
use super::ThreadItem;
|
||||||
|
use super::ThreadSection;
|
||||||
use super::ThreadSource;
|
use super::ThreadSource;
|
||||||
use super::Turn;
|
use super::Turn;
|
||||||
use super::TurnEnvironmentParams;
|
use super::TurnEnvironmentParams;
|
||||||
@@ -865,9 +866,15 @@ pub struct ThreadMetadataUpdateParams {
|
|||||||
/// provide a string to replace the stored value.
|
/// provide a string to replace the stored value.
|
||||||
#[ts(optional = nullable)]
|
#[ts(optional = nullable)]
|
||||||
pub git_info: Option<ThreadMetadataGitInfoUpdateParams>,
|
pub git_info: Option<ThreadMetadataGitInfoUpdateParams>,
|
||||||
/// Patch whether this thread is pinned. Omit to leave the stored value unchanged.
|
/// Omit to leave the section unchanged, set to `null` to clear it, or provide a section ID.
|
||||||
#[ts(optional = nullable)]
|
#[serde(
|
||||||
pub is_pinned: Option<bool>,
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
|
||||||
|
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option"
|
||||||
|
)]
|
||||||
|
#[ts(optional = nullable, type = "string | null")]
|
||||||
|
pub section_id: Option<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
@@ -1093,6 +1100,29 @@ pub struct ThreadRollbackResponse {
|
|||||||
pub thread: Thread,
|
pub thread: Thread,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parameters for listing independently persisted thread sections.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadSectionListParams {
|
||||||
|
/// Opaque pagination cursor returned by a previous call.
|
||||||
|
#[ts(optional = nullable)]
|
||||||
|
pub cursor: Option<String>,
|
||||||
|
/// Maximum number of sections to return.
|
||||||
|
#[ts(optional = nullable)]
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One page of independently persisted thread sections.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadSectionListResponse {
|
||||||
|
pub data: Vec<ThreadSection>,
|
||||||
|
/// Opaque cursor for the next page, or `null` when no sections remain.
|
||||||
|
pub next_cursor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
@@ -1121,9 +1151,16 @@ pub struct ThreadListParams {
|
|||||||
/// If false or null, only non-archived threads are returned.
|
/// If false or null, only non-archived threads are returned.
|
||||||
#[ts(optional = nullable)]
|
#[ts(optional = nullable)]
|
||||||
pub archived: Option<bool>,
|
pub archived: Option<bool>,
|
||||||
/// Optional pinned filter; when set, only threads matching this value are returned.
|
/// Omit to include every section, set to `null` for unsectioned threads,
|
||||||
#[ts(optional = nullable)]
|
/// or provide a section ID to return only threads in that section.
|
||||||
pub is_pinned: Option<bool>,
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
|
||||||
|
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option"
|
||||||
|
)]
|
||||||
|
#[ts(optional = nullable, type = "string | null")]
|
||||||
|
pub section_id: Option<Option<String>>,
|
||||||
/// Optional cwd filter or filters; when set, only threads whose session cwd
|
/// Optional cwd filter or filters; when set, only threads whose session cwd
|
||||||
/// exactly matches one of these paths are returned.
|
/// exactly matches one of these paths are returned.
|
||||||
#[ts(optional = nullable, type = "string | Array<string> | null")]
|
#[ts(optional = nullable, type = "string | Array<string> | null")]
|
||||||
|
|||||||
@@ -164,6 +164,17 @@ pub struct GitInfo {
|
|||||||
pub origin_url: Option<String>,
|
pub origin_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An independently persisted, user-visible thread section.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadSection {
|
||||||
|
/// Opaque UUIDv7 identity that remains stable when the section is renamed.
|
||||||
|
pub id: String,
|
||||||
|
/// The current user-visible section name.
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
@@ -183,9 +194,9 @@ pub struct Thread {
|
|||||||
pub preview: String,
|
pub preview: String,
|
||||||
/// Whether the thread is ephemeral and should not be materialized on disk.
|
/// Whether the thread is ephemeral and should not be materialized on disk.
|
||||||
pub ephemeral: bool,
|
pub ephemeral: bool,
|
||||||
/// Whether the thread has been pinned by the user.
|
/// The independently persisted section selected for this thread, if any.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub is_pinned: bool,
|
pub section: Option<ThreadSection>,
|
||||||
/// Persisted thread history contract selected when this thread was created.
|
/// Persisted thread history contract selected when this thread was created.
|
||||||
#[experimental("thread.historyMode")]
|
#[experimental("thread.historyMode")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -1308,7 +1308,7 @@ async fn thread_list(endpoint: &Endpoint, config_overrides: &[String], limit: u3
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
ancestor_thread_id: None,
|
ancestor_thread_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ impl fmt::Display for ConnectionId {
|
|||||||
/// Outgoing message from the server to the client.
|
/// Outgoing message from the server to the client.
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum OutgoingMessage {
|
pub enum OutgoingMessage {
|
||||||
Request(ServerRequest),
|
Request(ServerRequest),
|
||||||
/// AppServerNotification is specific to the case where this is run as an
|
/// AppServerNotification is specific to the case where this is run as an
|
||||||
|
|||||||
@@ -143,13 +143,14 @@ Example with notification opt-out:
|
|||||||
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
|
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
|
||||||
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; pass an optional `lastTurnId` to copy history only through that turn, inclusive, and drop later turns from the fork. An in-progress `lastTurnId` boundary is rejected. Experimental `beforeTurnId` instead copies history strictly before the referenced turn, including when that turn is in progress, and cannot be combined with `lastTurnId`. If both boundaries are null while the source thread is mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately, or `deferGoalContinuation: true` to carry the source thread's current goal into the fork and run an explicit turn before automatic continuation resumes. Deferred goal continuation is persisted until that turn starts and cannot be combined with `ephemeral: true`. Accepts the same permission override rules as `thread/start`.
|
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; pass an optional `lastTurnId` to copy history only through that turn, inclusive, and drop later turns from the fork. An in-progress `lastTurnId` boundary is rejected. Experimental `beforeTurnId` instead copies history strictly before the referenced turn, including when that turn is in progress, and cannot be combined with `lastTurnId`. If both boundaries are null while the source thread is mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately, or `deferGoalContinuation: true` to carry the source thread's current goal into the fork and run an explicit turn before automatic continuation resumes. Deferred goal continuation is persisted until that turn starts and cannot be combined with `ephemeral: true`. Accepts the same permission override rules as `thread/start`.
|
||||||
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their deprecated experimental `multiAgentMode` field, and the corresponding thread setting, always report `explicitRequestOnly`; Ultra reasoning effort is the source of proactive multi-agent behavior.
|
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their deprecated experimental `multiAgentMode` field, and the corresponding thread setting, always report `explicitRequestOnly`; Ultra reasoning effort is the source of proactive multi-agent behavior.
|
||||||
- `thread/list` — page through stored threads; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `isPinned`, `cwd`, and `searchTerm` filters. Experimental clients can use `parentThreadId` for direct spawned children or `ancestorThreadId` for spawned descendants at any depth; the two filters are mutually exclusive. Review and Guardian threads are not included because they do not participate in that spawn-edge lifecycle. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate parent is known.
|
- `thread/list` — page through stored threads; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `sectionId`, `cwd`, and `searchTerm` filters. Experimental clients can use `parentThreadId` for direct spawned children or `ancestorThreadId` for spawned descendants at any depth; the two filters are mutually exclusive. Review and Guardian threads are not included because they do not participate in that spawn-edge lifecycle. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate parent is known.
|
||||||
|
- `threadSection/list` — page through independently persisted thread sections and their display names, including sections that do not currently contain any threads.
|
||||||
- `thread/loaded/list` — list the thread ids currently loaded in memory.
|
- `thread/loaded/list` — list the thread ids currently loaded in memory.
|
||||||
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. For loaded threads, experimental clients can use `canAcceptDirectInput` to determine whether `turn/start` and `turn/steer` are accepted; unloaded stored threads report `null` when that capability is unavailable.
|
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. For loaded threads, experimental clients can use `canAcceptDirectInput` to determine whether `turn/start` and `turn/steer` are accepted; unloaded stored threads report `null` when that capability is unavailable.
|
||||||
- `thread/turns/list` — experimental; page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`.
|
- `thread/turns/list` — experimental; page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`.
|
||||||
- `thread/items/list` — experimental; page through persisted thread items without resuming the thread. Pass `turnId` to restrict results to one turn, or omit it to page items across the thread. The active thread store must support item pagination.
|
- `thread/items/list` — experimental; page through persisted thread items without resuming the thread. Pass `turnId` to restrict results to one turn, or omit it to page items across the thread. The active thread store must support item pagination.
|
||||||
- `thread/searchOccurrences` — experimental; find literal, case-insensitive matches in visible user messages and summary-selected final assistant messages within one paginated thread.
|
- `thread/searchOccurrences` — experimental; find literal, case-insensitive matches in visible user messages and summary-selected final assistant messages within one paginated thread.
|
||||||
- `thread/metadata/update` — patch stored thread metadata in sqlite; supports updating persisted `gitInfo` fields and `isPinned`, and returns the refreshed `thread`.
|
- `thread/metadata/update` — patch stored thread metadata in sqlite; supports updating persisted `gitInfo` fields and `sectionId`, and returns the refreshed `thread`. Set `sectionId` to the ID of an existing section, pass `null` to clear it, or omit it to leave the current section unchanged. The returned `thread.section` contains the section's `id` and display `name`, or `null` when the thread has no section.
|
||||||
- `thread/settings/update` — experimental; queue a partial update to a loaded thread’s next-turn settings without starting a turn or adding transcript items. Omitted fields leave settings unchanged; `serviceTier: null` clears the tier; deprecated `multiAgentMode` is ignored, while Ultra reasoning effort enables proactive multi-agent behavior; `sandboxPolicy` and `permissions` cannot be combined. Returns `{}` when the update is accepted and emits `thread/settings/updated` with the full effective settings only if they actually change. `turn/start` settings overrides emit the same notification when they change the stored settings.
|
- `thread/settings/update` — experimental; queue a partial update to a loaded thread’s next-turn settings without starting a turn or adding transcript items. Omitted fields leave settings unchanged; `serviceTier: null` clears the tier; deprecated `multiAgentMode` is ignored, while Ultra reasoning effort enables proactive multi-agent behavior; `sandboxPolicy` and `permissions` cannot be combined. Returns `{}` when the update is accepted and emits `thread/settings/updated` with the full effective settings only if they actually change. `turn/start` settings overrides emit the same notification when they change the stored settings.
|
||||||
- `thread/memoryMode/set` — experimental; set a thread’s persisted memory eligibility to `"enabled"` or `"disabled"` for either a loaded thread or a stored rollout; returns `{}` on success.
|
- `thread/memoryMode/set` — experimental; set a thread’s persisted memory eligibility to `"enabled"` or `"disabled"` for either a loaded thread or a stored rollout; returns `{}` on success.
|
||||||
- `memory/reset` — experimental; clear the current `CODEX_HOME/memories` directory and reset persisted memory stage data in sqlite while preserving existing thread memory modes; returns `{}` on success.
|
- `memory/reset` — experimental; clear the current `CODEX_HOME/memories` directory and reset persisted memory stage data in sqlite while preserving existing thread memory modes; returns `{}` on success.
|
||||||
@@ -392,7 +393,7 @@ Like `thread/resume`, experimental clients can pass `excludeTurns: true` to `thr
|
|||||||
- `modelProviders` — restrict results to specific providers; unset, null, or an empty array will include all providers.
|
- `modelProviders` — restrict results to specific providers; unset, null, or an empty array will include all providers.
|
||||||
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`).
|
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`).
|
||||||
- `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default).
|
- `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default).
|
||||||
- `isPinned` — when provided, return only threads whose persisted pin state matches the requested value; omit it to include both pinned and unpinned threads.
|
- `sectionId` — provide an ID from `threadSection/list` to return threads from that section; pass `null` to return only threads without a section; or omit it to include threads from every section and threads without a section.
|
||||||
- `cwd` — restrict results to threads whose session cwd exactly matches this path, or one of these paths when an array is provided. Relative paths are resolved against the app-server process cwd before matching.
|
- `cwd` — restrict results to threads whose session cwd exactly matches this path, or one of these paths when an array is provided. Relative paths are resolved against the app-server process cwd before matching.
|
||||||
- `useStateDbOnly` — when `true`, return from the state DB without scanning JSONL rollouts to repair metadata. Omit or pass `false` to preserve the default scan-and-repair behavior.
|
- `useStateDbOnly` — when `true`, return from the state DB without scanning JSONL rollouts to repair metadata. Omit or pass `false` to preserve the default scan-and-repair behavior.
|
||||||
- `searchTerm` — restrict results to threads whose extracted title contains this substring (case-sensitive).
|
- `searchTerm` — restrict results to threads whose extracted title contains this substring (case-sensitive).
|
||||||
|
|||||||
@@ -2217,7 +2217,7 @@ mod tests {
|
|||||||
updated_at: created_at,
|
updated_at: created_at,
|
||||||
recency_at: created_at,
|
recency_at: created_at,
|
||||||
archived_at: None,
|
archived_at: None,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: test_path_buf("/tmp").abs().into(),
|
cwd: test_path_buf("/tmp").abs().into(),
|
||||||
cli_version: "0.0.0".to_string(),
|
cli_version: "0.0.0".to_string(),
|
||||||
source: SessionSource::Cli,
|
source: SessionSource::Cli,
|
||||||
|
|||||||
@@ -1114,6 +1114,9 @@ impl MessageProcessor {
|
|||||||
ClientRequest::ThreadMetadataUpdate { params, .. } => {
|
ClientRequest::ThreadMetadataUpdate { params, .. } => {
|
||||||
self.thread_processor.thread_metadata_update(params).await
|
self.thread_processor.thread_metadata_update(params).await
|
||||||
}
|
}
|
||||||
|
ClientRequest::ThreadSectionList { params, .. } => {
|
||||||
|
self.thread_processor.thread_section_list(params).await
|
||||||
|
}
|
||||||
ClientRequest::ThreadSettingsUpdate { params, .. } => {
|
ClientRequest::ThreadSettingsUpdate { params, .. } => {
|
||||||
self.turn_processor
|
self.turn_processor
|
||||||
.thread_settings_update(&request_id, params)
|
.thread_settings_update(&request_id, params)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ use super::turn_processor::can_accept_direct_input;
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::error_code::method_not_found;
|
use crate::error_code::method_not_found;
|
||||||
use codex_app_server_protocol::SelectedCapabilityRoot;
|
use codex_app_server_protocol::SelectedCapabilityRoot;
|
||||||
|
use codex_app_server_protocol::ThreadSection;
|
||||||
|
use codex_app_server_protocol::ThreadSectionListParams;
|
||||||
|
use codex_app_server_protocol::ThreadSectionListResponse;
|
||||||
use codex_extension_api::ExtensionDataInit;
|
use codex_extension_api::ExtensionDataInit;
|
||||||
use codex_protocol::config_types::MultiAgentMode;
|
use codex_protocol::config_types::MultiAgentMode;
|
||||||
use codex_protocol::error::CodexErrorDetails;
|
use codex_protocol::error::CodexErrorDetails;
|
||||||
@@ -20,7 +23,7 @@ struct ThreadListFilters {
|
|||||||
model_providers: Option<Vec<String>>,
|
model_providers: Option<Vec<String>>,
|
||||||
source_kinds: Option<Vec<ThreadSourceKind>>,
|
source_kinds: Option<Vec<ThreadSourceKind>>,
|
||||||
archived: bool,
|
archived: bool,
|
||||||
is_pinned: Option<bool>,
|
section_id: Option<Option<String>>,
|
||||||
cwd_filters: Option<Vec<PathBuf>>,
|
cwd_filters: Option<Vec<PathBuf>>,
|
||||||
search_term: Option<String>,
|
search_term: Option<String>,
|
||||||
use_state_db_only: bool,
|
use_state_db_only: bool,
|
||||||
@@ -695,6 +698,39 @@ impl ThreadRequestProcessor {
|
|||||||
.map(|response| Some(response.into()))
|
.map(|response| Some(response.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn thread_section_list(
|
||||||
|
&self,
|
||||||
|
params: ThreadSectionListParams,
|
||||||
|
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||||
|
let limit = params
|
||||||
|
.limit
|
||||||
|
.map(|value| value as usize)
|
||||||
|
.unwrap_or(THREAD_LIST_DEFAULT_LIMIT)
|
||||||
|
.clamp(1, THREAD_LIST_MAX_LIMIT);
|
||||||
|
let state_db = self.state_db.clone().ok_or_else(|| {
|
||||||
|
method_not_found("threadSection/list is unavailable without sqlite state")
|
||||||
|
})?;
|
||||||
|
let page = state_db
|
||||||
|
.list_thread_sections(params.cursor.as_deref(), limit)
|
||||||
|
.await
|
||||||
|
.map_err(|err| internal_error(format!("failed to list thread sections: {err}")))?;
|
||||||
|
|
||||||
|
Ok(Some(
|
||||||
|
ThreadSectionListResponse {
|
||||||
|
data: page
|
||||||
|
.sections
|
||||||
|
.into_iter()
|
||||||
|
.map(|section| ThreadSection {
|
||||||
|
id: section.id,
|
||||||
|
name: section.name,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
next_cursor: page.next_cursor,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn thread_search(
|
pub(crate) async fn thread_search(
|
||||||
&self,
|
&self,
|
||||||
params: ThreadSearchParams,
|
params: ThreadSearchParams,
|
||||||
@@ -1638,13 +1674,13 @@ impl ThreadRequestProcessor {
|
|||||||
let ThreadMetadataUpdateParams {
|
let ThreadMetadataUpdateParams {
|
||||||
thread_id,
|
thread_id,
|
||||||
git_info,
|
git_info,
|
||||||
is_pinned,
|
section_id,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let thread_uuid = ThreadId::from_string(&thread_id)
|
let thread_uuid = ThreadId::from_string(&thread_id)
|
||||||
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))?;
|
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))?;
|
||||||
|
|
||||||
if git_info.is_none() && is_pinned.is_none() {
|
if git_info.is_none() && section_id.is_none() {
|
||||||
return Err(invalid_request(
|
return Err(invalid_request(
|
||||||
"thread metadata update must include at least one field",
|
"thread metadata update must include at least one field",
|
||||||
));
|
));
|
||||||
@@ -1676,14 +1712,13 @@ impl ThreadRequestProcessor {
|
|||||||
)
|
)
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
let patch = StoreThreadMetadataPatch {
|
|
||||||
git_info,
|
|
||||||
is_pinned,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let updated_thread = {
|
let updated_thread = {
|
||||||
let _thread_list_state_permit = self.acquire_thread_list_state_permit().await?;
|
let _thread_list_state_permit = self.acquire_thread_list_state_permit().await?;
|
||||||
|
let patch = StoreThreadMetadataPatch {
|
||||||
|
git_info,
|
||||||
|
section: section_id,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
self.thread_manager
|
self.thread_manager
|
||||||
.update_thread_metadata(thread_uuid, patch, /*include_archived*/ true)
|
.update_thread_metadata(thread_uuid, patch, /*include_archived*/ true)
|
||||||
.await
|
.await
|
||||||
@@ -1977,7 +2012,7 @@ impl ThreadRequestProcessor {
|
|||||||
model_providers,
|
model_providers,
|
||||||
source_kinds,
|
source_kinds,
|
||||||
archived,
|
archived,
|
||||||
is_pinned,
|
section_id,
|
||||||
cwd,
|
cwd,
|
||||||
use_state_db_only,
|
use_state_db_only,
|
||||||
search_term,
|
search_term,
|
||||||
@@ -2022,7 +2057,7 @@ impl ThreadRequestProcessor {
|
|||||||
model_providers,
|
model_providers,
|
||||||
source_kinds,
|
source_kinds,
|
||||||
archived: archived.unwrap_or(false),
|
archived: archived.unwrap_or(false),
|
||||||
is_pinned,
|
section_id,
|
||||||
cwd_filters,
|
cwd_filters,
|
||||||
search_term,
|
search_term,
|
||||||
use_state_db_only,
|
use_state_db_only,
|
||||||
@@ -4448,7 +4483,7 @@ impl ThreadRequestProcessor {
|
|||||||
model_providers,
|
model_providers,
|
||||||
source_kinds,
|
source_kinds,
|
||||||
archived,
|
archived,
|
||||||
is_pinned,
|
section_id,
|
||||||
cwd_filters,
|
cwd_filters,
|
||||||
search_term,
|
search_term,
|
||||||
use_state_db_only,
|
use_state_db_only,
|
||||||
@@ -4496,7 +4531,7 @@ impl ThreadRequestProcessor {
|
|||||||
model_providers: model_provider_filter.clone(),
|
model_providers: model_provider_filter.clone(),
|
||||||
cwd_filters: cwd_filters.clone(),
|
cwd_filters: cwd_filters.clone(),
|
||||||
archived,
|
archived,
|
||||||
is_pinned,
|
section: section_id.clone(),
|
||||||
search_term: search_term.clone(),
|
search_term: search_term.clone(),
|
||||||
use_state_db_only,
|
use_state_db_only,
|
||||||
relation_filter,
|
relation_filter,
|
||||||
@@ -5081,7 +5116,10 @@ pub(crate) fn thread_from_stored_thread(
|
|||||||
parent_thread_id: thread.parent_thread_id.map(|id| id.to_string()),
|
parent_thread_id: thread.parent_thread_id.map(|id| id.to_string()),
|
||||||
preview: thread.preview,
|
preview: thread.preview,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: thread.is_pinned,
|
section: thread.section.map(|section| ThreadSection {
|
||||||
|
id: section.id,
|
||||||
|
name: section.name,
|
||||||
|
}),
|
||||||
history_mode: thread.history_mode.into(),
|
history_mode: thread.history_mode.into(),
|
||||||
model_provider: if thread.model_provider.is_empty() {
|
model_provider: if thread.model_provider.is_empty() {
|
||||||
fallback_provider.to_string()
|
fallback_provider.to_string()
|
||||||
@@ -5293,7 +5331,7 @@ fn build_thread_from_snapshot(
|
|||||||
parent_thread_id: config_snapshot.parent_thread_id.map(|id| id.to_string()),
|
parent_thread_id: config_snapshot.parent_thread_id.map(|id| id.to_string()),
|
||||||
preview: String::new(),
|
preview: String::new(),
|
||||||
ephemeral: config_snapshot.ephemeral,
|
ephemeral: config_snapshot.ephemeral,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: config_snapshot.history_mode.into(),
|
history_mode: config_snapshot.history_mode.into(),
|
||||||
model_provider: config_snapshot.model_provider_id.clone(),
|
model_provider: config_snapshot.model_provider_id.clone(),
|
||||||
created_at: now,
|
created_at: now,
|
||||||
|
|||||||
@@ -480,7 +480,7 @@ mod thread_processor_behavior_tests {
|
|||||||
updated_at: updated_at.with_timezone(&Utc),
|
updated_at: updated_at.with_timezone(&Utc),
|
||||||
recency_at: updated_at.with_timezone(&Utc),
|
recency_at: updated_at.with_timezone(&Utc),
|
||||||
archived_at: None,
|
archived_at: None,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: PathBuf::from("/tmp"),
|
cwd: PathBuf::from("/tmp"),
|
||||||
cli_version: "0.0.0".to_string(),
|
cli_version: "0.0.0".to_string(),
|
||||||
source: SessionSource::Cli,
|
source: SessionSource::Cli,
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ mod tests {
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: "preview".to_string(),
|
preview: "preview".to_string(),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "mock_provider".to_string(),
|
model_provider: "mock_provider".to_string(),
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ pub(crate) fn summary_to_thread(
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview,
|
preview,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: ThreadHistoryMode::Legacy,
|
history_mode: ThreadHistoryMode::Legacy,
|
||||||
model_provider,
|
model_provider,
|
||||||
created_at: created_at.map(|dt| dt.timestamp()).unwrap_or(0),
|
created_at: created_at.map(|dt| dt.timestamp()).unwrap_or(0),
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ source = {:?}
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -1665,7 +1665,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: true,
|
use_state_db_only: true,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -1852,7 +1852,7 @@ required = true
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -1936,7 +1936,7 @@ async fn external_agent_config_import_accepts_detected_session_payload_after_res
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -2017,7 +2017,7 @@ async fn external_agent_config_import_skips_already_imported_session_versions()
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -2144,7 +2144,7 @@ async fn external_agent_config_import_returns_before_background_session_import_f
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -2259,7 +2259,7 @@ async fn external_agent_config_import_compacts_huge_session_before_first_follow_
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ use codex_app_server_protocol::ThreadHistoryMode;
|
|||||||
use codex_app_server_protocol::ThreadListParams;
|
use codex_app_server_protocol::ThreadListParams;
|
||||||
use codex_app_server_protocol::ThreadListResponse;
|
use codex_app_server_protocol::ThreadListResponse;
|
||||||
use codex_app_server_protocol::ThreadResumeParams;
|
use codex_app_server_protocol::ThreadResumeParams;
|
||||||
|
use codex_app_server_protocol::ThreadSectionListParams;
|
||||||
use codex_app_server_protocol::ThreadStartParams;
|
use codex_app_server_protocol::ThreadStartParams;
|
||||||
use codex_app_server_protocol::ThreadStartResponse;
|
use codex_app_server_protocol::ThreadStartResponse;
|
||||||
use codex_app_server_protocol::TurnStartParams;
|
use codex_app_server_protocol::TurnStartParams;
|
||||||
@@ -66,6 +67,41 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_section_list_without_sqlite_returns_method_not_found() -> Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
let store_id = Uuid::new_v4().to_string();
|
||||||
|
create_config_toml_with_thread_store(codex_home.path(), "http://127.0.0.1:1", &store_id)?;
|
||||||
|
let _in_memory_store = InMemoryThreadStoreId { store_id };
|
||||||
|
let client = start_in_process_server(codex_home.path()).await?;
|
||||||
|
|
||||||
|
let error = client
|
||||||
|
.request(ClientRequest::ThreadSectionList {
|
||||||
|
request_id: RequestId::Integer(1),
|
||||||
|
params: ThreadSectionListParams::default(),
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.expect_err("section discovery requires sqlite state");
|
||||||
|
|
||||||
|
assert_eq!(error.code, -32601);
|
||||||
|
assert_eq!(
|
||||||
|
error.message,
|
||||||
|
"threadSection/list is unavailable without sqlite state"
|
||||||
|
);
|
||||||
|
|
||||||
|
client.shutdown().await?;
|
||||||
|
assert!(
|
||||||
|
!codex_home.path().join("state_5.sqlite").exists(),
|
||||||
|
"section discovery must not create local SQLite state"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!codex_home.path().join("sessions").exists(),
|
||||||
|
"section discovery must not create local thread persistence"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn thread_start_rejects_paginated_history_without_list_support() -> Result<()> {
|
async fn thread_start_rejects_paginated_history_without_list_support() -> Result<()> {
|
||||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||||
@@ -177,7 +213,7 @@ async fn thread_delete_with_non_local_thread_store_does_not_create_local_persist
|
|||||||
model_providers: Some(Vec::new()),
|
model_providers: Some(Vec::new()),
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ async fn list_threads(mcp: &mut TestAppServer) -> Result<ThreadListResponse> {
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ async fn list_threads_with_sort(
|
|||||||
model_providers: providers,
|
model_providers: providers,
|
||||||
source_kinds,
|
source_kinds,
|
||||||
archived,
|
archived,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -134,7 +134,7 @@ async fn list_threads_for_relation(
|
|||||||
model_providers,
|
model_providers,
|
||||||
source_kinds,
|
source_kinds,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -538,7 +538,7 @@ async fn thread_list_respects_cwd_filters() -> Result<()> {
|
|||||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: Some(ThreadListCwdFilter::Many(vec![
|
cwd: Some(ThreadListCwdFilter::Many(vec![
|
||||||
first_target_cwd.to_string_lossy().into_owned(),
|
first_target_cwd.to_string_lossy().into_owned(),
|
||||||
second_target_cwd.to_string_lossy().into_owned(),
|
second_target_cwd.to_string_lossy().into_owned(),
|
||||||
@@ -650,7 +650,7 @@ sqlite = true
|
|||||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: Some("needle".to_string()),
|
search_term: Some("needle".to_string()),
|
||||||
@@ -853,7 +853,7 @@ sqlite = true
|
|||||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -888,7 +888,7 @@ sqlite = true
|
|||||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: Some(ThreadListCwdFilter::One(
|
cwd: Some(ThreadListCwdFilter::One(
|
||||||
stale_cwd.to_string_lossy().into_owned(),
|
stale_cwd.to_string_lossy().into_owned(),
|
||||||
)),
|
)),
|
||||||
@@ -916,7 +916,7 @@ sqlite = true
|
|||||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: Some(ThreadListCwdFilter::One(
|
cwd: Some(ThreadListCwdFilter::One(
|
||||||
stale_cwd.to_string_lossy().into_owned(),
|
stale_cwd.to_string_lossy().into_owned(),
|
||||||
)),
|
)),
|
||||||
@@ -1100,7 +1100,7 @@ async fn thread_list_relation_filters_reject_invalid_requests() -> Result<()> {
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -1125,7 +1125,7 @@ async fn thread_list_relation_filters_reject_invalid_requests() -> Result<()> {
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -1917,7 +1917,7 @@ async fn thread_list_backwards_cursor_can_seed_forward_delta_sync() -> Result<()
|
|||||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -1957,7 +1957,7 @@ async fn thread_list_backwards_cursor_can_seed_forward_delta_sync() -> Result<()
|
|||||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -2193,7 +2193,7 @@ async fn thread_list_invalid_cursor_returns_error() -> Result<()> {
|
|||||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ use codex_app_server_protocol::ThreadReadParams;
|
|||||||
use codex_app_server_protocol::ThreadReadResponse;
|
use codex_app_server_protocol::ThreadReadResponse;
|
||||||
use codex_app_server_protocol::ThreadResumeParams;
|
use codex_app_server_protocol::ThreadResumeParams;
|
||||||
use codex_app_server_protocol::ThreadResumeResponse;
|
use codex_app_server_protocol::ThreadResumeResponse;
|
||||||
|
use codex_app_server_protocol::ThreadSection;
|
||||||
|
use codex_app_server_protocol::ThreadSectionListParams;
|
||||||
|
use codex_app_server_protocol::ThreadSectionListResponse;
|
||||||
use codex_app_server_protocol::ThreadSortKey;
|
use codex_app_server_protocol::ThreadSortKey;
|
||||||
use codex_app_server_protocol::ThreadStartParams;
|
use codex_app_server_protocol::ThreadStartParams;
|
||||||
use codex_app_server_protocol::ThreadStartResponse;
|
use codex_app_server_protocol::ThreadStartResponse;
|
||||||
@@ -28,6 +31,8 @@ use codex_git_utils::GitSha;
|
|||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
use codex_protocol::protocol::GitInfo as RolloutGitInfo;
|
use codex_protocol::protocol::GitInfo as RolloutGitInfo;
|
||||||
use codex_rollout::state_db::reconcile_rollout;
|
use codex_rollout::state_db::reconcile_rollout;
|
||||||
|
use codex_state::PINNED_THREAD_SECTION_ID;
|
||||||
|
use codex_state::PINNED_THREAD_SECTION_NAME;
|
||||||
use codex_state::StateRuntime;
|
use codex_state::StateRuntime;
|
||||||
use codex_utils_absolute_path::test_support::PathExt;
|
use codex_utils_absolute_path::test_support::PathExt;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
@@ -92,12 +97,49 @@ async fn thread_metadata_update_pins_and_unpins_with_filtered_recency_pagination
|
|||||||
.await?;
|
.await?;
|
||||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||||
|
|
||||||
|
let pinned_section = ThreadSection {
|
||||||
|
id: PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
name: PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
};
|
||||||
|
let section_list_id = mcp
|
||||||
|
.send_raw_request(
|
||||||
|
"threadSection/list",
|
||||||
|
Some(serde_json::to_value(ThreadSectionListParams {
|
||||||
|
cursor: None,
|
||||||
|
limit: Some(1),
|
||||||
|
})?),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let empty_membership_sections: ThreadSectionListResponse =
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(section_list_id)).await??;
|
||||||
|
assert_eq!(empty_membership_sections.data, vec![pinned_section.clone()]);
|
||||||
|
assert_eq!(empty_membership_sections.next_cursor, None);
|
||||||
|
|
||||||
|
let unknown_section_id = "01984de2-8f74-7c91-a3b2-5c5e937cf319";
|
||||||
|
let unknown_section_request_id = mcp
|
||||||
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
|
thread_id: initially_unpinned.clone(),
|
||||||
|
git_info: None,
|
||||||
|
section_id: Some(Some(unknown_section_id.to_string())),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let unknown_section_error: JSONRPCError = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_error_message(RequestId::Integer(unknown_section_request_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
assert_eq!(unknown_section_error.error.code, INVALID_REQUEST_ERROR_CODE);
|
||||||
|
assert_eq!(
|
||||||
|
unknown_section_error.error.message,
|
||||||
|
format!("thread section not found: {unknown_section_id}")
|
||||||
|
);
|
||||||
|
|
||||||
for thread_id in [older_pinned, newer_pinned] {
|
for thread_id in [older_pinned, newer_pinned] {
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
thread_id: thread_id.clone(),
|
thread_id: thread_id.clone(),
|
||||||
git_info: None,
|
git_info: None,
|
||||||
is_pinned: Some(true),
|
section_id: Some(Some(PINNED_THREAD_SECTION_ID.to_string())),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let response = timeout(
|
let response = timeout(
|
||||||
@@ -105,15 +147,15 @@ async fn thread_metadata_update_pins_and_unpins_with_filtered_recency_pagination
|
|||||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
let wire_is_pinned = response
|
let wire_section = response
|
||||||
.result
|
.result
|
||||||
.get("thread")
|
.get("thread")
|
||||||
.and_then(|thread| thread.get("isPinned"))
|
.and_then(|thread| thread.get("section"))
|
||||||
.and_then(Value::as_bool);
|
.cloned();
|
||||||
let ThreadMetadataUpdateResponse { thread } = to_response(response)?;
|
let ThreadMetadataUpdateResponse { thread } = to_response(response)?;
|
||||||
assert_eq!(thread.id, *thread_id);
|
assert_eq!(thread.id, *thread_id);
|
||||||
assert!(thread.is_pinned);
|
assert_eq!(thread.section, Some(pinned_section.clone()));
|
||||||
assert_eq!(wire_is_pinned, Some(true));
|
assert_eq!(wire_section, Some(serde_json::to_value(&pinned_section)?));
|
||||||
}
|
}
|
||||||
|
|
||||||
let list_params = ThreadListParams {
|
let list_params = ThreadListParams {
|
||||||
@@ -124,7 +166,7 @@ async fn thread_metadata_update_pins_and_unpins_with_filtered_recency_pagination
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: Some(true),
|
section_id: Some(Some(PINNED_THREAD_SECTION_ID.to_string())),
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -140,7 +182,7 @@ async fn thread_metadata_update_pins_and_unpins_with_filtered_recency_pagination
|
|||||||
let first_page: ThreadListResponse = to_response(response)?;
|
let first_page: ThreadListResponse = to_response(response)?;
|
||||||
assert_eq!(first_page.data.len(), 1);
|
assert_eq!(first_page.data.len(), 1);
|
||||||
assert_eq!(first_page.data[0].id, *newer_pinned);
|
assert_eq!(first_page.data[0].id, *newer_pinned);
|
||||||
assert!(first_page.data[0].is_pinned);
|
assert_eq!(first_page.data[0].section, Some(pinned_section.clone()));
|
||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
@@ -156,13 +198,13 @@ async fn thread_metadata_update_pins_and_unpins_with_filtered_recency_pagination
|
|||||||
let second_page: ThreadListResponse = to_response(response)?;
|
let second_page: ThreadListResponse = to_response(response)?;
|
||||||
assert_eq!(second_page.data.len(), 1);
|
assert_eq!(second_page.data.len(), 1);
|
||||||
assert_eq!(second_page.data[0].id, *older_pinned);
|
assert_eq!(second_page.data[0].id, *older_pinned);
|
||||||
assert!(second_page.data[0].is_pinned);
|
assert_eq!(second_page.data[0].section, Some(pinned_section.clone()));
|
||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
thread_id: newer_pinned.clone(),
|
thread_id: newer_pinned.clone(),
|
||||||
git_info: None,
|
git_info: None,
|
||||||
is_pinned: Some(false),
|
section_id: Some(None),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let response = timeout(
|
let response = timeout(
|
||||||
@@ -171,12 +213,12 @@ async fn thread_metadata_update_pins_and_unpins_with_filtered_recency_pagination
|
|||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
let ThreadMetadataUpdateResponse { thread } = to_response(response)?;
|
let ThreadMetadataUpdateResponse { thread } = to_response(response)?;
|
||||||
assert!(!thread.is_pinned);
|
assert_eq!(thread.section, None);
|
||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
is_pinned: Some(false),
|
section_id: Some(None),
|
||||||
..list_params
|
..list_params
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -185,16 +227,35 @@ async fn thread_metadata_update_pins_and_unpins_with_filtered_recency_pagination
|
|||||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
let unpinned_page: ThreadListResponse = to_response(response)?;
|
let unsectioned_page: ThreadListResponse = to_response(response)?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unpinned_page
|
unsectioned_page
|
||||||
.data
|
.data
|
||||||
.iter()
|
.iter()
|
||||||
.map(|thread| thread.id.as_str())
|
.map(|thread| thread.id.as_str())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
[newer_pinned.as_str(), initially_unpinned.as_str()]
|
[newer_pinned.as_str(), initially_unpinned.as_str()]
|
||||||
);
|
);
|
||||||
assert!(unpinned_page.data.iter().all(|thread| !thread.is_pinned));
|
assert!(
|
||||||
|
unsectioned_page
|
||||||
|
.data
|
||||||
|
.iter()
|
||||||
|
.all(|thread| thread.section.is_none())
|
||||||
|
);
|
||||||
|
|
||||||
|
let section_list_id = mcp
|
||||||
|
.send_raw_request(
|
||||||
|
"threadSection/list",
|
||||||
|
Some(serde_json::to_value(ThreadSectionListParams {
|
||||||
|
cursor: None,
|
||||||
|
limit: Some(1),
|
||||||
|
})?),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let sections_after_clear: ThreadSectionListResponse =
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(section_list_id)).await??;
|
||||||
|
assert_eq!(sections_after_clear.data, vec![pinned_section]);
|
||||||
|
assert_eq!(sections_after_clear.next_cursor, None);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -227,7 +288,7 @@ async fn thread_metadata_update_patches_git_branch_and_returns_updated_thread()
|
|||||||
let update_id = mcp
|
let update_id = mcp
|
||||||
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
thread_id: thread.id.clone(),
|
thread_id: thread.id.clone(),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
||||||
sha: None,
|
sha: None,
|
||||||
branch: Some(Some("feature/sidebar-pr".to_string())),
|
branch: Some(Some("feature/sidebar-pr".to_string())),
|
||||||
@@ -326,7 +387,7 @@ async fn thread_metadata_update_rejects_empty_git_info_patch() -> Result<()> {
|
|||||||
let update_id = mcp
|
let update_id = mcp
|
||||||
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
thread_id: thread.id,
|
thread_id: thread.id,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
||||||
sha: None,
|
sha: None,
|
||||||
branch: None,
|
branch: None,
|
||||||
@@ -377,7 +438,7 @@ async fn thread_metadata_update_rejects_ephemeral_thread() -> Result<()> {
|
|||||||
let update_id = mcp
|
let update_id = mcp
|
||||||
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
thread_id: thread.id.clone(),
|
thread_id: thread.id.clone(),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
||||||
sha: None,
|
sha: None,
|
||||||
branch: Some(Some("feature/ephemeral".to_string())),
|
branch: Some(Some("feature/ephemeral".to_string())),
|
||||||
@@ -400,6 +461,28 @@ async fn thread_metadata_update_rejects_ephemeral_thread() -> Result<()> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let clear_section_id = mcp
|
||||||
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
|
thread_id: thread.id.clone(),
|
||||||
|
section_id: Some(None),
|
||||||
|
git_info: None,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let clear_section_err: JSONRPCError = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_error_message(RequestId::Integer(clear_section_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
assert_eq!(clear_section_err.error.code, INVALID_REQUEST_ERROR_CODE);
|
||||||
|
assert_eq!(
|
||||||
|
clear_section_err.error.message,
|
||||||
|
format!(
|
||||||
|
"ephemeral thread does not support metadata updates: {}",
|
||||||
|
thread.id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +513,7 @@ async fn thread_metadata_update_repairs_missing_sqlite_row_for_stored_thread() -
|
|||||||
let update_id = mcp
|
let update_id = mcp
|
||||||
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
thread_id: thread_id.clone(),
|
thread_id: thread_id.clone(),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
||||||
sha: None,
|
sha: None,
|
||||||
branch: Some(Some("feature/stored-thread".to_string())),
|
branch: Some(Some("feature/stored-thread".to_string())),
|
||||||
@@ -515,7 +598,7 @@ async fn thread_metadata_update_repairs_loaded_thread_without_resetting_summary(
|
|||||||
let update_id = mcp
|
let update_id = mcp
|
||||||
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
thread_id: thread_id.clone(),
|
thread_id: thread_id.clone(),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
||||||
sha: None,
|
sha: None,
|
||||||
branch: Some(Some("feature/loaded-thread".to_string())),
|
branch: Some(Some("feature/loaded-thread".to_string())),
|
||||||
@@ -583,7 +666,7 @@ async fn thread_metadata_update_repairs_missing_sqlite_row_for_archived_thread()
|
|||||||
let update_id = mcp
|
let update_id = mcp
|
||||||
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
thread_id: thread_id.clone(),
|
thread_id: thread_id.clone(),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
||||||
sha: None,
|
sha: None,
|
||||||
branch: Some(Some("feature/archived-thread".to_string())),
|
branch: Some(Some("feature/archived-thread".to_string())),
|
||||||
@@ -644,7 +727,7 @@ async fn thread_metadata_update_can_clear_stored_git_fields() -> Result<()> {
|
|||||||
let update_id = mcp
|
let update_id = mcp
|
||||||
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
thread_id: thread_id.clone(),
|
thread_id: thread_id.clone(),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
||||||
sha: Some(None),
|
sha: Some(None),
|
||||||
branch: Some(None),
|
branch: Some(None),
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ async fn paginated_stored_thread_routes_projected_turns_and_rejects_legacy_histo
|
|||||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -955,7 +955,7 @@ async fn thread_list_includes_store_thread_without_rollout_path() -> Result<()>
|
|||||||
model_providers: Some(Vec::new()),
|
model_providers: Some(Vec::new()),
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
@@ -1334,7 +1334,7 @@ async fn paginated_thread_name_set_is_reflected_in_read_list_and_metadata_resume
|
|||||||
model_providers: Some(vec!["mock_provider".to_string()]),
|
model_providers: Some(vec!["mock_provider".to_string()]),
|
||||||
source_kinds: None,
|
source_kinds: None,
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: true,
|
use_state_db_only: true,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
|
|||||||
@@ -2693,7 +2693,7 @@ async fn thread_resume_prefers_persisted_git_metadata_for_local_threads() -> Res
|
|||||||
let update_id = mcp
|
let update_id = mcp
|
||||||
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
thread_id: thread_id.clone(),
|
thread_id: thread_id.clone(),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
||||||
sha: None,
|
sha: None,
|
||||||
branch: Some(Some("feature/pr-branch".to_string())),
|
branch: Some(Some("feature/pr-branch".to_string())),
|
||||||
@@ -2723,7 +2723,6 @@ async fn thread_resume_prefers_persisted_git_metadata_for_local_threads() -> Res
|
|||||||
.and_then(|git| git.branch.as_deref()),
|
.and_then(|git| git.branch.as_deref()),
|
||||||
Some("feature/pr-branch")
|
Some("feature/pr-branch")
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3021,7 +3020,6 @@ async fn thread_resume_keeps_in_flight_turn_streaming() -> Result<()> {
|
|||||||
..
|
..
|
||||||
} = timeout(DEFAULT_READ_TIMEOUT, secondary.read_response(resume_id)).await??;
|
} = timeout(DEFAULT_READ_TIMEOUT, secondary.read_response(resume_id)).await??;
|
||||||
assert_ne!(resumed_thread.status, ThreadStatus::NotLoaded);
|
assert_ne!(resumed_thread.status, ThreadStatus::NotLoaded);
|
||||||
|
|
||||||
timeout(
|
timeout(
|
||||||
DEFAULT_READ_TIMEOUT,
|
DEFAULT_READ_TIMEOUT,
|
||||||
primary.read_stream_until_notification_message("turn/completed"),
|
primary.read_stream_until_notification_message("turn/completed"),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use codex_app_server_protocol::ThreadArchiveParams;
|
|||||||
use codex_app_server_protocol::ThreadArchiveResponse;
|
use codex_app_server_protocol::ThreadArchiveResponse;
|
||||||
use codex_app_server_protocol::ThreadMetadataUpdateParams;
|
use codex_app_server_protocol::ThreadMetadataUpdateParams;
|
||||||
use codex_app_server_protocol::ThreadMetadataUpdateResponse;
|
use codex_app_server_protocol::ThreadMetadataUpdateResponse;
|
||||||
|
use codex_app_server_protocol::ThreadSection;
|
||||||
use codex_app_server_protocol::ThreadStartParams;
|
use codex_app_server_protocol::ThreadStartParams;
|
||||||
use codex_app_server_protocol::ThreadStartResponse;
|
use codex_app_server_protocol::ThreadStartResponse;
|
||||||
use codex_app_server_protocol::ThreadStatus;
|
use codex_app_server_protocol::ThreadStatus;
|
||||||
@@ -36,6 +37,8 @@ use codex_protocol::ThreadId;
|
|||||||
use codex_protocol::models::BaseInstructions;
|
use codex_protocol::models::BaseInstructions;
|
||||||
use codex_protocol::protocol::SessionSource;
|
use codex_protocol::protocol::SessionSource;
|
||||||
use codex_protocol::protocol::ThreadMemoryMode;
|
use codex_protocol::protocol::ThreadMemoryMode;
|
||||||
|
use codex_state::PINNED_THREAD_SECTION_ID;
|
||||||
|
use codex_state::PINNED_THREAD_SECTION_NAME;
|
||||||
use codex_thread_store::CreateThreadParams;
|
use codex_thread_store::CreateThreadParams;
|
||||||
use codex_thread_store::InMemoryThreadStore;
|
use codex_thread_store::InMemoryThreadStore;
|
||||||
use codex_thread_store::ThreadMetadataPatch;
|
use codex_thread_store::ThreadMetadataPatch;
|
||||||
@@ -97,17 +100,21 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result
|
|||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
|
let pinned_section = ThreadSection {
|
||||||
|
id: PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
name: PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
};
|
||||||
let pin_id = mcp
|
let pin_id = mcp
|
||||||
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
.send_thread_metadata_update_request(ThreadMetadataUpdateParams {
|
||||||
thread_id: thread.id.clone(),
|
thread_id: thread.id.clone(),
|
||||||
git_info: None,
|
git_info: None,
|
||||||
is_pinned: Some(true),
|
section_id: Some(Some(PINNED_THREAD_SECTION_ID.to_string())),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let ThreadMetadataUpdateResponse {
|
let ThreadMetadataUpdateResponse {
|
||||||
thread: pinned_thread,
|
thread: pinned_thread,
|
||||||
} = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(pin_id)).await??;
|
} = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(pin_id)).await??;
|
||||||
assert!(pinned_thread.is_pinned);
|
assert_eq!(pinned_thread.section, Some(pinned_section.clone()));
|
||||||
|
|
||||||
let found_rollout_path =
|
let found_rollout_path =
|
||||||
find_thread_path_by_id_str(codex_home.path(), &thread.id, /*state_db_ctx*/ None)
|
find_thread_path_by_id_str(codex_home.path(), &thread.id, /*state_db_ctx*/ None)
|
||||||
@@ -166,7 +173,7 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result
|
|||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
assert_eq!(unarchived_notification.thread_id, thread.id);
|
assert_eq!(unarchived_notification.thread_id, thread.id);
|
||||||
assert!(unarchived_thread.is_pinned);
|
assert_eq!(unarchived_thread.section, Some(pinned_section.clone()));
|
||||||
assert!(
|
assert!(
|
||||||
unarchived_thread.updated_at > old_timestamp,
|
unarchived_thread.updated_at > old_timestamp,
|
||||||
"expected updated_at to be bumped on unarchive"
|
"expected updated_at to be bumped on unarchive"
|
||||||
@@ -179,7 +186,10 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result
|
|||||||
.and_then(Value::as_object)
|
.and_then(Value::as_object)
|
||||||
.expect("thread/unarchive result.thread must be an object");
|
.expect("thread/unarchive result.thread must be an object");
|
||||||
assert_eq!(unarchived_thread.name, None);
|
assert_eq!(unarchived_thread.name, None);
|
||||||
assert_eq!(thread_json.get("isPinned"), Some(&Value::Bool(true)));
|
assert_eq!(
|
||||||
|
thread_json.get("section"),
|
||||||
|
Some(&serde_json::to_value(&pinned_section)?)
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
thread_json.get("name"),
|
thread_json.get("name"),
|
||||||
Some(&Value::Null),
|
Some(&Value::Null),
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ async fn load_recent_threads(sess: &Session) -> Vec<StoredThread> {
|
|||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
archived: false,
|
archived: false,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
use_state_db_only: false,
|
use_state_db_only: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ fn stored_thread(cwd: &str, title: &str, first_user_message: &str) -> StoredThre
|
|||||||
.single()
|
.single()
|
||||||
.expect("valid timestamp"),
|
.expect("valid timestamp"),
|
||||||
archived_at: None,
|
archived_at: None,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: PathBuf::from(cwd),
|
cwd: PathBuf::from(cwd),
|
||||||
cli_version: "test".to_string(),
|
cli_version: "test".to_string(),
|
||||||
source: SessionSource::Cli,
|
source: SessionSource::Cli,
|
||||||
|
|||||||
@@ -1485,7 +1485,7 @@ async fn resolve_resume_thread_id(
|
|||||||
model_providers: model_providers.clone(),
|
model_providers: model_providers.clone(),
|
||||||
source_kinds: Some(all_thread_source_kinds()),
|
source_kinds: Some(all_thread_source_kinds()),
|
||||||
archived: Some(false),
|
archived: Some(false),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
ancestor_thread_id: None,
|
ancestor_thread_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
@@ -1553,7 +1553,7 @@ async fn resolve_resume_thread_id(
|
|||||||
model_providers: model_providers.clone(),
|
model_providers: model_providers.clone(),
|
||||||
source_kinds: Some(all_thread_source_kinds()),
|
source_kinds: Some(all_thread_source_kinds()),
|
||||||
archived: Some(false),
|
archived: Some(false),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
ancestor_thread_id: None,
|
ancestor_thread_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: String::new(),
|
preview: String::new(),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "openai".to_string(),
|
model_provider: "openai".to_string(),
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
@@ -803,7 +803,7 @@ fn sample_thread_start_response() -> ThreadStartResponse {
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: String::new(),
|
preview: String::new(),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "openai".to_string(),
|
model_provider: "openai".to_string(),
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ pub struct ThreadItem {
|
|||||||
pub first_user_message: Option<String>,
|
pub first_user_message: Option<String>,
|
||||||
/// Best available user-facing preview for discovery and list display.
|
/// Best available user-facing preview for discovery and list display.
|
||||||
pub preview: Option<String>,
|
pub preview: Option<String>,
|
||||||
/// Whether the thread is pinned in SQLite-owned metadata.
|
/// The user-selected section in SQLite-owned metadata.
|
||||||
pub is_pinned: bool,
|
pub section: Option<codex_state::ThreadSection>,
|
||||||
/// Working directory from session metadata.
|
/// Working directory from session metadata.
|
||||||
pub cwd: Option<PathBuf>,
|
pub cwd: Option<PathBuf>,
|
||||||
/// Git branch from session metadata.
|
/// Git branch from session metadata.
|
||||||
@@ -832,7 +832,7 @@ async fn build_thread_item(
|
|||||||
thread_id,
|
thread_id,
|
||||||
first_user_message,
|
first_user_message,
|
||||||
preview,
|
preview,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd,
|
cwd,
|
||||||
git_branch,
|
git_branch,
|
||||||
git_sha,
|
git_sha,
|
||||||
|
|||||||
@@ -472,7 +472,7 @@ impl RolloutRecorder {
|
|||||||
cwd_filters,
|
cwd_filters,
|
||||||
/*relation_filter*/ None,
|
/*relation_filter*/ None,
|
||||||
archived,
|
archived,
|
||||||
/*is_pinned*/ None,
|
/*section*/ None,
|
||||||
search_term,
|
search_term,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -582,7 +582,7 @@ impl RolloutRecorder {
|
|||||||
cwd_filters,
|
cwd_filters,
|
||||||
/*relation_filter*/ None,
|
/*relation_filter*/ None,
|
||||||
archived,
|
archived,
|
||||||
/*is_pinned*/ None,
|
/*section*/ None,
|
||||||
search_term,
|
search_term,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -612,7 +612,7 @@ impl RolloutRecorder {
|
|||||||
cwd_filters,
|
cwd_filters,
|
||||||
/*relation_filter*/ None,
|
/*relation_filter*/ None,
|
||||||
archived,
|
archived,
|
||||||
/*is_pinned*/ None,
|
/*section*/ None,
|
||||||
search_term,
|
search_term,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -653,7 +653,7 @@ impl RolloutRecorder {
|
|||||||
cwd_filters,
|
cwd_filters,
|
||||||
/*relation_filter*/ None,
|
/*relation_filter*/ None,
|
||||||
archived,
|
archived,
|
||||||
/*is_pinned*/ None,
|
/*section*/ None,
|
||||||
search_term,
|
search_term,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -733,7 +733,7 @@ impl RolloutRecorder {
|
|||||||
cwd_filter.as_ref().map(std::slice::from_ref),
|
cwd_filter.as_ref().map(std::slice::from_ref),
|
||||||
/*relation_filter*/ None,
|
/*relation_filter*/ None,
|
||||||
/*archived*/ false,
|
/*archived*/ false,
|
||||||
/*is_pinned*/ None,
|
/*section*/ None,
|
||||||
/*search_term*/ None,
|
/*search_term*/ None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1202,7 +1202,7 @@ fn fill_missing_thread_item_metadata(item: &mut ThreadItem, state_item: ThreadIt
|
|||||||
thread_id: _state_thread_id,
|
thread_id: _state_thread_id,
|
||||||
first_user_message,
|
first_user_message,
|
||||||
preview,
|
preview,
|
||||||
is_pinned,
|
section,
|
||||||
cwd,
|
cwd,
|
||||||
git_branch,
|
git_branch,
|
||||||
git_sha,
|
git_sha,
|
||||||
@@ -1225,7 +1225,7 @@ fn fill_missing_thread_item_metadata(item: &mut ThreadItem, state_item: ThreadIt
|
|||||||
if item.preview.is_none() {
|
if item.preview.is_none() {
|
||||||
item.preview = preview;
|
item.preview = preview;
|
||||||
}
|
}
|
||||||
item.is_pinned = is_pinned;
|
item.section = section;
|
||||||
if item.cwd.is_none() {
|
if item.cwd.is_none() {
|
||||||
item.cwd = cwd;
|
item.cwd = cwd;
|
||||||
}
|
}
|
||||||
@@ -1961,7 +1961,7 @@ fn thread_item_from_state_metadata(
|
|||||||
thread_id: Some(item.id),
|
thread_id: Some(item.id),
|
||||||
first_user_message: item.first_user_message,
|
first_user_message: item.first_user_message,
|
||||||
preview: item.preview,
|
preview: item.preview,
|
||||||
is_pinned: item.is_pinned,
|
section: item.section,
|
||||||
cwd: Some(item.cwd),
|
cwd: Some(item.cwd),
|
||||||
git_branch: item.git_branch,
|
git_branch: item.git_branch,
|
||||||
git_sha: item.git_sha,
|
git_sha: item.git_sha,
|
||||||
|
|||||||
@@ -1437,7 +1437,7 @@ fn fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fi
|
|||||||
thread_id: Some(filesystem_thread_id),
|
thread_id: Some(filesystem_thread_id),
|
||||||
first_user_message: Some("filesystem message".to_string()),
|
first_user_message: Some("filesystem message".to_string()),
|
||||||
preview: Some("filesystem preview".to_string()),
|
preview: Some("filesystem preview".to_string()),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
git_branch: Some("filesystem-branch".to_string()),
|
git_branch: Some("filesystem-branch".to_string()),
|
||||||
git_sha: Some("filesystem-sha".to_string()),
|
git_sha: Some("filesystem-sha".to_string()),
|
||||||
@@ -1458,7 +1458,10 @@ fn fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fi
|
|||||||
thread_id: Some(state_thread_id),
|
thread_id: Some(state_thread_id),
|
||||||
first_user_message: Some("state message".to_string()),
|
first_user_message: Some("state message".to_string()),
|
||||||
preview: Some("state preview".to_string()),
|
preview: Some("state preview".to_string()),
|
||||||
is_pinned: true,
|
section: Some(codex_state::ThreadSection {
|
||||||
|
id: codex_state::PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
}),
|
||||||
cwd: Some(PathBuf::from("/tmp/state-cwd")),
|
cwd: Some(PathBuf::from("/tmp/state-cwd")),
|
||||||
git_branch: Some("state-branch".to_string()),
|
git_branch: Some("state-branch".to_string()),
|
||||||
git_sha: Some("state-sha".to_string()),
|
git_sha: Some("state-sha".to_string()),
|
||||||
@@ -1479,7 +1482,13 @@ fn fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fi
|
|||||||
|
|
||||||
assert_eq!(item.path, filesystem_path);
|
assert_eq!(item.path, filesystem_path);
|
||||||
assert_eq!(item.thread_id, Some(filesystem_thread_id));
|
assert_eq!(item.thread_id, Some(filesystem_thread_id));
|
||||||
assert!(item.is_pinned);
|
assert_eq!(
|
||||||
|
item.section,
|
||||||
|
Some(codex_state::ThreadSection {
|
||||||
|
id: codex_state::PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
item.first_user_message.as_deref(),
|
item.first_user_message.as_deref(),
|
||||||
Some("filesystem message")
|
Some("filesystem message")
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ pub async fn list_threads_db(
|
|||||||
cwd_filters: Option<&[PathBuf]>,
|
cwd_filters: Option<&[PathBuf]>,
|
||||||
relation_filter: Option<codex_state::ThreadRelationFilter>,
|
relation_filter: Option<codex_state::ThreadRelationFilter>,
|
||||||
archived: bool,
|
archived: bool,
|
||||||
is_pinned: Option<bool>,
|
section: Option<Option<&str>>,
|
||||||
search_term: Option<&str>,
|
search_term: Option<&str>,
|
||||||
) -> Option<codex_state::ThreadsPage> {
|
) -> Option<codex_state::ThreadsPage> {
|
||||||
let ctx = context?;
|
let ctx = context?;
|
||||||
@@ -403,7 +403,7 @@ pub async fn list_threads_db(
|
|||||||
if let Some(relation_filter) = relation_filter {
|
if let Some(relation_filter) = relation_filter {
|
||||||
let filters = codex_state::ThreadFilterOptions {
|
let filters = codex_state::ThreadFilterOptions {
|
||||||
archived_only: archived,
|
archived_only: archived,
|
||||||
is_pinned,
|
section,
|
||||||
allowed_sources: allowed_sources.as_slice(),
|
allowed_sources: allowed_sources.as_slice(),
|
||||||
model_providers: model_providers.as_deref(),
|
model_providers: model_providers.as_deref(),
|
||||||
cwd_filters: normalized_cwd_filters.as_deref(),
|
cwd_filters: normalized_cwd_filters.as_deref(),
|
||||||
@@ -430,7 +430,7 @@ pub async fn list_threads_db(
|
|||||||
while items.len() < page_size {
|
while items.len() < page_size {
|
||||||
let filters = codex_state::ThreadFilterOptions {
|
let filters = codex_state::ThreadFilterOptions {
|
||||||
archived_only: archived,
|
archived_only: archived,
|
||||||
is_pinned,
|
section,
|
||||||
allowed_sources: allowed_sources.as_slice(),
|
allowed_sources: allowed_sources.as_slice(),
|
||||||
model_providers: model_providers.as_deref(),
|
model_providers: model_providers.as_deref(),
|
||||||
cwd_filters: normalized_cwd_filters.as_deref(),
|
cwd_filters: normalized_cwd_filters.as_deref(),
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ async fn list_threads_db_rejects_mismatched_sqlite_config_without_cleanup() -> a
|
|||||||
/*cwd_filters*/ None,
|
/*cwd_filters*/ None,
|
||||||
/*relation_filter*/ None,
|
/*relation_filter*/ None,
|
||||||
/*archived*/ false,
|
/*archived*/ false,
|
||||||
/*is_pinned*/ None,
|
/*section*/ None,
|
||||||
/*search_term*/ None,
|
/*search_term*/ None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -650,7 +650,7 @@ async fn test_list_conversations_latest_first() {
|
|||||||
thread_id: Some(thread_id_from_uuid(u3)),
|
thread_id: Some(thread_id_from_uuid(u3)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
preview: Some("Hello from user".to_string()),
|
preview: Some("Hello from user".to_string()),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: Some(Path::new(".").to_path_buf()),
|
cwd: Some(Path::new(".").to_path_buf()),
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
@@ -671,7 +671,7 @@ async fn test_list_conversations_latest_first() {
|
|||||||
thread_id: Some(thread_id_from_uuid(u2)),
|
thread_id: Some(thread_id_from_uuid(u2)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
preview: Some("Hello from user".to_string()),
|
preview: Some("Hello from user".to_string()),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: Some(Path::new(".").to_path_buf()),
|
cwd: Some(Path::new(".").to_path_buf()),
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
@@ -692,7 +692,7 @@ async fn test_list_conversations_latest_first() {
|
|||||||
thread_id: Some(thread_id_from_uuid(u1)),
|
thread_id: Some(thread_id_from_uuid(u1)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
preview: Some("Hello from user".to_string()),
|
preview: Some("Hello from user".to_string()),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: Some(Path::new(".").to_path_buf()),
|
cwd: Some(Path::new(".").to_path_buf()),
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
@@ -806,7 +806,7 @@ async fn test_pagination_cursor() {
|
|||||||
thread_id: Some(thread_id_from_uuid(u5)),
|
thread_id: Some(thread_id_from_uuid(u5)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
preview: Some("Hello from user".to_string()),
|
preview: Some("Hello from user".to_string()),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: Some(Path::new(".").to_path_buf()),
|
cwd: Some(Path::new(".").to_path_buf()),
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
@@ -827,7 +827,7 @@ async fn test_pagination_cursor() {
|
|||||||
thread_id: Some(thread_id_from_uuid(u4)),
|
thread_id: Some(thread_id_from_uuid(u4)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
preview: Some("Hello from user".to_string()),
|
preview: Some("Hello from user".to_string()),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: Some(Path::new(".").to_path_buf()),
|
cwd: Some(Path::new(".").to_path_buf()),
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
@@ -884,7 +884,7 @@ async fn test_pagination_cursor() {
|
|||||||
thread_id: Some(thread_id_from_uuid(u3)),
|
thread_id: Some(thread_id_from_uuid(u3)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
preview: Some("Hello from user".to_string()),
|
preview: Some("Hello from user".to_string()),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: Some(Path::new(".").to_path_buf()),
|
cwd: Some(Path::new(".").to_path_buf()),
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
@@ -905,7 +905,7 @@ async fn test_pagination_cursor() {
|
|||||||
thread_id: Some(thread_id_from_uuid(u2)),
|
thread_id: Some(thread_id_from_uuid(u2)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
preview: Some("Hello from user".to_string()),
|
preview: Some("Hello from user".to_string()),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: Some(Path::new(".").to_path_buf()),
|
cwd: Some(Path::new(".").to_path_buf()),
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
@@ -954,7 +954,7 @@ async fn test_pagination_cursor() {
|
|||||||
thread_id: Some(thread_id_from_uuid(u1)),
|
thread_id: Some(thread_id_from_uuid(u1)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
preview: Some("Hello from user".to_string()),
|
preview: Some("Hello from user".to_string()),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: Some(Path::new(".").to_path_buf()),
|
cwd: Some(Path::new(".").to_path_buf()),
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
@@ -1128,7 +1128,7 @@ async fn test_get_thread_contents() {
|
|||||||
thread_id: Some(thread_id_from_uuid(uuid)),
|
thread_id: Some(thread_id_from_uuid(uuid)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
preview: Some("Hello from user".to_string()),
|
preview: Some("Hello from user".to_string()),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: Some(Path::new(".").to_path_buf()),
|
cwd: Some(Path::new(".").to_path_buf()),
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
@@ -1496,7 +1496,7 @@ async fn test_timestamp_only_cursor_skips_same_second_filesystem_ties() {
|
|||||||
thread_id: Some(thread_id_from_uuid(u3)),
|
thread_id: Some(thread_id_from_uuid(u3)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
preview: Some("Hello from user".to_string()),
|
preview: Some("Hello from user".to_string()),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: Some(Path::new(".").to_path_buf()),
|
cwd: Some(Path::new(".").to_path_buf()),
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
@@ -1517,7 +1517,7 @@ async fn test_timestamp_only_cursor_skips_same_second_filesystem_ties() {
|
|||||||
thread_id: Some(thread_id_from_uuid(u2)),
|
thread_id: Some(thread_id_from_uuid(u2)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
preview: Some("Hello from user".to_string()),
|
preview: Some("Hello from user".to_string()),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: Some(Path::new(".").to_path_buf()),
|
cwd: Some(Path::new(".").to_path_buf()),
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE thread_sections (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO thread_sections (id, name)
|
||||||
|
VALUES ('01984de2-8f74-7c91-a3b2-5c5e937cf318', 'Pinned');
|
||||||
|
|
||||||
|
ALTER TABLE threads ADD COLUMN thread_section_id TEXT
|
||||||
|
REFERENCES thread_sections(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_threads_section_recency_at_ms
|
||||||
|
ON threads(archived, thread_section_id, recency_at_ms DESC, id DESC)
|
||||||
|
WHERE thread_section_id IS NOT NULL AND preview <> '';
|
||||||
@@ -695,7 +695,7 @@ mod tests {
|
|||||||
tokens_used: 1,
|
tokens_used: 1,
|
||||||
first_user_message: None,
|
first_user_message: None,
|
||||||
archived_at: None,
|
archived_at: None,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_origin_url: None,
|
git_origin_url: None,
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ pub use model::ThreadGoalStatus;
|
|||||||
pub use model::ThreadMetadata;
|
pub use model::ThreadMetadata;
|
||||||
pub use model::ThreadMetadataBuilder;
|
pub use model::ThreadMetadataBuilder;
|
||||||
pub use model::ThreadRelationFilter;
|
pub use model::ThreadRelationFilter;
|
||||||
|
pub use model::ThreadSection;
|
||||||
|
pub use model::ThreadSectionsPage;
|
||||||
pub use model::ThreadsPage;
|
pub use model::ThreadsPage;
|
||||||
pub use runtime::ExternalAgentConfigImportDetailsRecord;
|
pub use runtime::ExternalAgentConfigImportDetailsRecord;
|
||||||
pub use runtime::ExternalAgentConfigImportFailureRecord;
|
pub use runtime::ExternalAgentConfigImportFailureRecord;
|
||||||
@@ -78,6 +80,12 @@ pub use telemetry::install_process_db_telemetry;
|
|||||||
pub use telemetry::record_backfill_gate;
|
pub use telemetry::record_backfill_gate;
|
||||||
pub use telemetry::record_fallback;
|
pub use telemetry::record_fallback;
|
||||||
|
|
||||||
|
/// Stable UUIDv7 identifying the built-in pinned thread section.
|
||||||
|
pub const PINNED_THREAD_SECTION_ID: &str = "01984de2-8f74-7c91-a3b2-5c5e937cf318";
|
||||||
|
|
||||||
|
/// User-facing name of the built-in pinned thread section.
|
||||||
|
pub const PINNED_THREAD_SECTION_NAME: &str = "Pinned";
|
||||||
|
|
||||||
/// Environment variable for overriding the SQLite state database home directory.
|
/// Environment variable for overriding the SQLite state database home directory.
|
||||||
pub const SQLITE_HOME_ENV: &str = "CODEX_SQLITE_HOME";
|
pub const SQLITE_HOME_ENV: &str = "CODEX_SQLITE_HOME";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use codex_utils_absolute_path::test_support::PathExt;
|
use codex_utils_absolute_path::test_support::PathExt;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use sqlx::Connection;
|
use sqlx::Connection;
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
use sqlx::migrate::Migration;
|
use sqlx::migrate::Migration;
|
||||||
@@ -8,6 +9,10 @@ use std::borrow::Cow;
|
|||||||
use super::STATE_MIGRATOR;
|
use super::STATE_MIGRATOR;
|
||||||
use super::THREAD_HISTORY_MIGRATOR;
|
use super::THREAD_HISTORY_MIGRATOR;
|
||||||
use super::repair_legacy_recency_migration_version;
|
use super::repair_legacy_recency_migration_version;
|
||||||
|
use crate::PINNED_THREAD_SECTION_ID;
|
||||||
|
use crate::PINNED_THREAD_SECTION_NAME;
|
||||||
|
|
||||||
|
const CUSTOM_THREAD_SECTION_ID: &str = "01984de2-8f74-7c91-a3b2-5c5e937cf317";
|
||||||
|
|
||||||
fn migrator_through(version: i64) -> Migrator {
|
fn migrator_through(version: i64) -> Migrator {
|
||||||
Migrator {
|
Migrator {
|
||||||
@@ -28,7 +33,7 @@ fn migrator_through(version: i64) -> Migrator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn pinned_threads_migration_defaults_existing_and_legacy_rows_to_unpinned() {
|
async fn thread_section_migration_preserves_legacy_pin_compatibility() {
|
||||||
let sqlite_home = crate::runtime::test_support::unique_temp_dir();
|
let sqlite_home = crate::runtime::test_support::unique_temp_dir();
|
||||||
tokio::fs::create_dir_all(&sqlite_home)
|
tokio::fs::create_dir_all(&sqlite_home)
|
||||||
.await
|
.await
|
||||||
@@ -42,20 +47,25 @@ async fn pinned_threads_migration_defaults_existing_and_legacy_rows_to_unpinned(
|
|||||||
.open_read_write_pool(&state_path)
|
.open_read_write_pool(&state_path)
|
||||||
.await
|
.await
|
||||||
.expect("sqlite database should open");
|
.expect("sqlite database should open");
|
||||||
migrator_through(/*version*/ 42)
|
migrator_through(/*version*/ 44)
|
||||||
.run(&pool)
|
.run(&pool)
|
||||||
.await
|
.await
|
||||||
.expect("pre-pin migrations should apply");
|
.expect("released thread migrations should apply");
|
||||||
|
|
||||||
for thread_id in [
|
for thread_id in [
|
||||||
"00000000-0000-0000-0000-000000000043",
|
"00000000-0000-0000-0000-000000000043",
|
||||||
"00000000-0000-0000-0000-000000000044",
|
"00000000-0000-0000-0000-000000000044",
|
||||||
] {
|
] {
|
||||||
if thread_id.ends_with("44") {
|
if thread_id.ends_with("44") {
|
||||||
|
sqlx::query("UPDATE threads SET is_pinned = 1 WHERE id = ?")
|
||||||
|
.bind("00000000-0000-0000-0000-000000000043")
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.expect("legacy pin should remain writable before section migration");
|
||||||
STATE_MIGRATOR
|
STATE_MIGRATOR
|
||||||
.run(&pool)
|
.run(&pool)
|
||||||
.await
|
.await
|
||||||
.expect("pin migration should apply");
|
.expect("section migration should apply");
|
||||||
}
|
}
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -92,11 +102,86 @@ INSERT INTO threads (
|
|||||||
.expect("legacy thread insert should succeed");
|
.expect("legacy thread insert should succeed");
|
||||||
}
|
}
|
||||||
|
|
||||||
let pinned_values = sqlx::query_scalar::<_, bool>("SELECT is_pinned FROM threads ORDER BY id")
|
let registered_sections =
|
||||||
.fetch_all(&pool)
|
sqlx::query_as::<_, (String, String)>("SELECT id, name FROM thread_sections ORDER BY id")
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await
|
||||||
|
.expect("independent thread sections should load");
|
||||||
|
assert_eq!(
|
||||||
|
registered_sections,
|
||||||
|
vec![(
|
||||||
|
PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
|
||||||
|
let threads = sqlx::query_as::<_, (i64, Option<String>)>(
|
||||||
|
"SELECT is_pinned, thread_section_id FROM threads ORDER BY id",
|
||||||
|
)
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await
|
||||||
|
.expect("legacy and section-aware thread metadata should load");
|
||||||
|
assert_eq!(threads, vec![(1, None), (0, None)]);
|
||||||
|
|
||||||
|
sqlx::query("INSERT INTO thread_sections (id, name) VALUES (?, ?)")
|
||||||
|
.bind(CUSTOM_THREAD_SECTION_ID)
|
||||||
|
.bind("Custom section")
|
||||||
|
.execute(&pool)
|
||||||
.await
|
.await
|
||||||
.expect("pin states should load");
|
.expect("custom sections should have independent persisted identities");
|
||||||
assert_eq!(pinned_values, vec![false, false]);
|
|
||||||
|
let thread_id = "00000000-0000-0000-0000-000000000043";
|
||||||
|
sqlx::query("UPDATE threads SET thread_section_id = ? WHERE id = ?")
|
||||||
|
.bind(CUSTOM_THREAD_SECTION_ID)
|
||||||
|
.bind(thread_id)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.expect("threads should reference independently persisted sections");
|
||||||
|
sqlx::query("UPDATE threads SET is_pinned = 0 WHERE id = ?")
|
||||||
|
.bind(thread_id)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.expect("released binaries should still update the legacy pin column");
|
||||||
|
let thread = sqlx::query_as::<_, (i64, Option<String>)>(
|
||||||
|
"SELECT is_pinned, thread_section_id FROM threads WHERE id = ?",
|
||||||
|
)
|
||||||
|
.bind(thread_id)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
.expect("legacy pin updates should not overwrite the authoritative section");
|
||||||
|
assert_eq!(thread, (0, Some(CUSTOM_THREAD_SECTION_ID.to_string())));
|
||||||
|
|
||||||
|
sqlx::query("UPDATE threads SET thread_section_id = NULL WHERE id = ?")
|
||||||
|
.bind(thread_id)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.expect("threads should be removable from sections");
|
||||||
|
|
||||||
|
let registered_sections =
|
||||||
|
sqlx::query_as::<_, (String, String)>("SELECT id, name FROM thread_sections ORDER BY id")
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await
|
||||||
|
.expect("empty sections should remain independently discoverable");
|
||||||
|
assert_eq!(
|
||||||
|
registered_sections,
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
CUSTOM_THREAD_SECTION_ID.to_string(),
|
||||||
|
"Custom section".to_string(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut released_pin_migrator = migrator_through(/*version*/ 44);
|
||||||
|
released_pin_migrator.ignore_missing = true;
|
||||||
|
released_pin_migrator
|
||||||
|
.run(&pool)
|
||||||
|
.await
|
||||||
|
.expect("released pin-capable binaries should tolerate newer migrations");
|
||||||
|
|
||||||
pool.close().await;
|
pool.close().await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ pub use thread_metadata::SortKey;
|
|||||||
pub use thread_metadata::ThreadMetadata;
|
pub use thread_metadata::ThreadMetadata;
|
||||||
pub use thread_metadata::ThreadMetadataBuilder;
|
pub use thread_metadata::ThreadMetadataBuilder;
|
||||||
pub use thread_metadata::ThreadRelationFilter;
|
pub use thread_metadata::ThreadRelationFilter;
|
||||||
|
pub use thread_metadata::ThreadSection;
|
||||||
|
pub use thread_metadata::ThreadSectionsPage;
|
||||||
pub use thread_metadata::ThreadsPage;
|
pub use thread_metadata::ThreadsPage;
|
||||||
|
|
||||||
pub(crate) use thread_goal::ThreadGoalRow;
|
pub(crate) use thread_goal::ThreadGoalRow;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use codex_protocol::protocol::SandboxPolicy;
|
|||||||
use codex_protocol::protocol::SessionSource;
|
use codex_protocol::protocol::SessionSource;
|
||||||
use codex_protocol::protocol::ThreadHistoryMode;
|
use codex_protocol::protocol::ThreadHistoryMode;
|
||||||
use codex_protocol::protocol::ThreadSource;
|
use codex_protocol::protocol::ThreadSource;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
use sqlx::sqlite::SqliteRow;
|
use sqlx::sqlite::SqliteRow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -49,6 +51,24 @@ pub struct Anchor {
|
|||||||
pub id: Option<ThreadId>,
|
pub id: Option<ThreadId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An independently persisted thread section and its user-facing name.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ThreadSection {
|
||||||
|
/// Opaque UUIDv7 identifying the section independently of its name.
|
||||||
|
pub id: String,
|
||||||
|
/// User-facing section name.
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cursor-paginated page of independently persisted thread sections.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ThreadSectionsPage {
|
||||||
|
/// Sections in ascending identifier order.
|
||||||
|
pub sections: Vec<ThreadSection>,
|
||||||
|
/// Identifier after which the next page starts, if any.
|
||||||
|
pub next_cursor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// A single page of thread metadata results.
|
/// A single page of thread metadata results.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ThreadsPage {
|
pub struct ThreadsPage {
|
||||||
@@ -124,8 +144,8 @@ pub struct ThreadMetadata {
|
|||||||
pub first_user_message: Option<String>,
|
pub first_user_message: Option<String>,
|
||||||
/// The archive timestamp, if the thread is archived.
|
/// The archive timestamp, if the thread is archived.
|
||||||
pub archived_at: Option<DateTime<Utc>>,
|
pub archived_at: Option<DateTime<Utc>>,
|
||||||
/// Whether the thread was explicitly pinned by the user.
|
/// The user-selected section for this thread, if any.
|
||||||
pub is_pinned: bool,
|
pub section: Option<ThreadSection>,
|
||||||
/// The git commit SHA, if known.
|
/// The git commit SHA, if known.
|
||||||
pub git_sha: Option<String>,
|
pub git_sha: Option<String>,
|
||||||
/// The git branch name, if known.
|
/// The git branch name, if known.
|
||||||
@@ -256,7 +276,7 @@ impl ThreadMetadataBuilder {
|
|||||||
tokens_used: 0,
|
tokens_used: 0,
|
||||||
first_user_message: None,
|
first_user_message: None,
|
||||||
archived_at: self.archived_at.map(canonicalize_datetime),
|
archived_at: self.archived_at.map(canonicalize_datetime),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
git_sha: self.git_sha.clone(),
|
git_sha: self.git_sha.clone(),
|
||||||
git_branch: self.git_branch.clone(),
|
git_branch: self.git_branch.clone(),
|
||||||
git_origin_url: self.git_origin_url.clone(),
|
git_origin_url: self.git_origin_url.clone(),
|
||||||
@@ -371,8 +391,8 @@ impl ThreadMetadata {
|
|||||||
if self.archived_at != other.archived_at {
|
if self.archived_at != other.archived_at {
|
||||||
diffs.push("archived_at");
|
diffs.push("archived_at");
|
||||||
}
|
}
|
||||||
if self.is_pinned != other.is_pinned {
|
if self.section != other.section {
|
||||||
diffs.push("is_pinned");
|
diffs.push("section");
|
||||||
}
|
}
|
||||||
if self.git_sha != other.git_sha {
|
if self.git_sha != other.git_sha {
|
||||||
diffs.push("git_sha");
|
diffs.push("git_sha");
|
||||||
@@ -417,7 +437,8 @@ pub(crate) struct ThreadRow {
|
|||||||
tokens_used: i64,
|
tokens_used: i64,
|
||||||
first_user_message: String,
|
first_user_message: String,
|
||||||
archived_at: Option<i64>,
|
archived_at: Option<i64>,
|
||||||
is_pinned: bool,
|
section: Option<String>,
|
||||||
|
section_name: Option<String>,
|
||||||
git_sha: Option<String>,
|
git_sha: Option<String>,
|
||||||
git_branch: Option<String>,
|
git_branch: Option<String>,
|
||||||
git_origin_url: Option<String>,
|
git_origin_url: Option<String>,
|
||||||
@@ -450,7 +471,8 @@ impl ThreadRow {
|
|||||||
tokens_used: row.try_get("tokens_used")?,
|
tokens_used: row.try_get("tokens_used")?,
|
||||||
first_user_message: row.try_get("first_user_message")?,
|
first_user_message: row.try_get("first_user_message")?,
|
||||||
archived_at: row.try_get("archived_at")?,
|
archived_at: row.try_get("archived_at")?,
|
||||||
is_pinned: row.try_get("is_pinned")?,
|
section: row.try_get("section")?,
|
||||||
|
section_name: row.try_get("section_name")?,
|
||||||
git_sha: row.try_get("git_sha")?,
|
git_sha: row.try_get("git_sha")?,
|
||||||
git_branch: row.try_get("git_branch")?,
|
git_branch: row.try_get("git_branch")?,
|
||||||
git_origin_url: row.try_get("git_origin_url")?,
|
git_origin_url: row.try_get("git_origin_url")?,
|
||||||
@@ -487,7 +509,8 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
|||||||
tokens_used,
|
tokens_used,
|
||||||
first_user_message,
|
first_user_message,
|
||||||
archived_at,
|
archived_at,
|
||||||
is_pinned,
|
section,
|
||||||
|
section_name,
|
||||||
git_sha,
|
git_sha,
|
||||||
git_branch,
|
git_branch,
|
||||||
git_origin_url,
|
git_origin_url,
|
||||||
@@ -497,6 +520,20 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
|||||||
.transpose()
|
.transpose()
|
||||||
.map_err(anyhow::Error::msg)?;
|
.map_err(anyhow::Error::msg)?;
|
||||||
let history_mode = history_mode.parse().map_err(anyhow::Error::msg)?;
|
let history_mode = history_mode.parse().map_err(anyhow::Error::msg)?;
|
||||||
|
let section = match (section, section_name) {
|
||||||
|
(Some(id), Some(name)) => Some(ThreadSection { id, name }),
|
||||||
|
(None, None) => None,
|
||||||
|
(Some(id), None) => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"thread references an unknown section: {id}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
(None, Some(name)) => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"thread has a section name without a section id: {name}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: ThreadId::try_from(id)?,
|
id: ThreadId::try_from(id)?,
|
||||||
rollout_path: PathBuf::from(rollout_path),
|
rollout_path: PathBuf::from(rollout_path),
|
||||||
@@ -523,7 +560,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
|||||||
tokens_used,
|
tokens_used,
|
||||||
first_user_message: (!first_user_message.is_empty()).then_some(first_user_message),
|
first_user_message: (!first_user_message.is_empty()).then_some(first_user_message),
|
||||||
archived_at: archived_at.map(epoch_seconds_to_datetime).transpose()?,
|
archived_at: archived_at.map(epoch_seconds_to_datetime).transpose()?,
|
||||||
is_pinned,
|
section,
|
||||||
git_sha,
|
git_sha,
|
||||||
git_branch,
|
git_branch,
|
||||||
git_origin_url,
|
git_origin_url,
|
||||||
@@ -622,7 +659,8 @@ mod tests {
|
|||||||
tokens_used: 1,
|
tokens_used: 1,
|
||||||
first_user_message: String::new(),
|
first_user_message: String::new(),
|
||||||
archived_at: None,
|
archived_at: None,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
|
section_name: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_origin_url: None,
|
git_origin_url: None,
|
||||||
@@ -656,7 +694,7 @@ mod tests {
|
|||||||
tokens_used: 1,
|
tokens_used: 1,
|
||||||
first_user_message: None,
|
first_user_message: None,
|
||||||
archived_at: None,
|
archived_at: None,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_origin_url: None,
|
git_origin_url: None,
|
||||||
|
|||||||
@@ -195,7 +195,12 @@ SELECT
|
|||||||
threads.tokens_used,
|
threads.tokens_used,
|
||||||
threads.first_user_message,
|
threads.first_user_message,
|
||||||
threads.archived_at,
|
threads.archived_at,
|
||||||
threads.is_pinned,
|
threads.thread_section_id AS section,
|
||||||
|
(
|
||||||
|
SELECT thread_sections.name
|
||||||
|
FROM thread_sections
|
||||||
|
WHERE thread_sections.id = threads.thread_section_id
|
||||||
|
) AS section_name,
|
||||||
threads.git_sha,
|
threads.git_sha,
|
||||||
threads.git_branch,
|
threads.git_branch,
|
||||||
threads.git_origin_url
|
threads.git_origin_url
|
||||||
@@ -209,7 +214,7 @@ FROM threads
|
|||||||
allowed_sources,
|
allowed_sources,
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor: None,
|
anchor: None,
|
||||||
sort_key: SortKey::UpdatedAt,
|
sort_key: SortKey::UpdatedAt,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
@@ -571,7 +576,12 @@ SELECT
|
|||||||
threads.tokens_used,
|
threads.tokens_used,
|
||||||
threads.first_user_message,
|
threads.first_user_message,
|
||||||
threads.archived_at,
|
threads.archived_at,
|
||||||
threads.is_pinned,
|
threads.thread_section_id AS section,
|
||||||
|
(
|
||||||
|
SELECT thread_sections.name
|
||||||
|
FROM thread_sections
|
||||||
|
WHERE thread_sections.id = threads.thread_section_id
|
||||||
|
) AS section_name,
|
||||||
threads.git_sha,
|
threads.git_sha,
|
||||||
threads.git_branch,
|
threads.git_branch,
|
||||||
threads.git_origin_url
|
threads.git_origin_url
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ pub(super) fn test_thread_metadata(
|
|||||||
tokens_used: 0,
|
tokens_used: 0,
|
||||||
first_user_message: Some("hello".to_string()),
|
first_user_message: Some("hello".to_string()),
|
||||||
archived_at: None,
|
archived_at: None,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
git_sha: None,
|
git_sha: None,
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
git_origin_url: None,
|
git_origin_url: None,
|
||||||
|
|||||||
@@ -33,7 +33,12 @@ SELECT
|
|||||||
threads.tokens_used,
|
threads.tokens_used,
|
||||||
threads.first_user_message,
|
threads.first_user_message,
|
||||||
threads.archived_at,
|
threads.archived_at,
|
||||||
threads.is_pinned,
|
threads.thread_section_id AS section,
|
||||||
|
(
|
||||||
|
SELECT thread_sections.name
|
||||||
|
FROM thread_sections
|
||||||
|
WHERE thread_sections.id = threads.thread_section_id
|
||||||
|
) AS section_name,
|
||||||
threads.git_sha,
|
threads.git_sha,
|
||||||
threads.git_branch,
|
threads.git_branch,
|
||||||
threads.git_origin_url
|
threads.git_origin_url
|
||||||
@@ -48,6 +53,58 @@ WHERE threads.id = ?
|
|||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read an independently persisted thread section by its opaque identifier.
|
||||||
|
pub async fn get_thread_section(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
) -> anyhow::Result<Option<crate::ThreadSection>> {
|
||||||
|
let row = sqlx::query_as::<_, (String, String)>(
|
||||||
|
"SELECT id, name FROM thread_sections WHERE id = ?",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(self.pool.as_ref())
|
||||||
|
.await?;
|
||||||
|
Ok(row.map(|(id, name)| crate::ThreadSection { id, name }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List independently persisted sections in stable, cursor-paginated identifier order.
|
||||||
|
pub async fn list_thread_sections(
|
||||||
|
&self,
|
||||||
|
cursor: Option<&str>,
|
||||||
|
limit: usize,
|
||||||
|
) -> anyhow::Result<crate::ThreadSectionsPage> {
|
||||||
|
let page_size = limit.max(1);
|
||||||
|
let fetch_limit = i64::try_from(page_size.saturating_add(1))?;
|
||||||
|
let rows = sqlx::query_as::<_, (String, String)>(
|
||||||
|
r#"
|
||||||
|
SELECT id, name
|
||||||
|
FROM thread_sections
|
||||||
|
WHERE (? IS NULL OR id > ?)
|
||||||
|
ORDER BY id
|
||||||
|
LIMIT ?
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(cursor)
|
||||||
|
.bind(cursor)
|
||||||
|
.bind(fetch_limit)
|
||||||
|
.fetch_all(self.pool.as_ref())
|
||||||
|
.await?;
|
||||||
|
let mut sections = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, name)| crate::ThreadSection { id, name })
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let next_cursor = if sections.len() > page_size {
|
||||||
|
sections.pop();
|
||||||
|
sections.last().map(|section| section.id.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(crate::ThreadSectionsPage {
|
||||||
|
sections,
|
||||||
|
next_cursor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_thread_memory_mode(&self, id: ThreadId) -> anyhow::Result<Option<String>> {
|
pub async fn get_thread_memory_mode(&self, id: ThreadId) -> anyhow::Result<Option<String>> {
|
||||||
let row = sqlx::query("SELECT memory_mode FROM threads WHERE id = ?")
|
let row = sqlx::query("SELECT memory_mode FROM threads WHERE id = ?")
|
||||||
.bind(id.to_string())
|
.bind(id.to_string())
|
||||||
@@ -373,7 +430,7 @@ ON CONFLICT(child_thread_id) DO NOTHING
|
|||||||
allowed_sources,
|
allowed_sources,
|
||||||
model_providers,
|
model_providers,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor: None,
|
anchor: None,
|
||||||
sort_key: crate::SortKey::UpdatedAt,
|
sort_key: crate::SortKey::UpdatedAt,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
@@ -498,7 +555,7 @@ ON CONFLICT(child_thread_id) DO NOTHING
|
|||||||
allowed_sources,
|
allowed_sources,
|
||||||
model_providers,
|
model_providers,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor,
|
anchor,
|
||||||
sort_key,
|
sort_key,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
@@ -568,7 +625,7 @@ INSERT INTO threads (
|
|||||||
first_user_message,
|
first_user_message,
|
||||||
archived,
|
archived,
|
||||||
archived_at,
|
archived_at,
|
||||||
is_pinned,
|
thread_section_id,
|
||||||
git_sha,
|
git_sha,
|
||||||
git_branch,
|
git_branch,
|
||||||
git_origin_url,
|
git_origin_url,
|
||||||
@@ -615,7 +672,7 @@ ON CONFLICT(id) DO NOTHING
|
|||||||
.bind(metadata.first_user_message.as_deref().unwrap_or_default())
|
.bind(metadata.first_user_message.as_deref().unwrap_or_default())
|
||||||
.bind(metadata.archived_at.is_some())
|
.bind(metadata.archived_at.is_some())
|
||||||
.bind(metadata.archived_at.map(datetime_to_epoch_seconds))
|
.bind(metadata.archived_at.map(datetime_to_epoch_seconds))
|
||||||
.bind(metadata.is_pinned)
|
.bind(metadata.section.as_ref().map(|section| section.id.as_str()))
|
||||||
.bind(metadata.git_sha.as_deref())
|
.bind(metadata.git_sha.as_deref())
|
||||||
.bind(metadata.git_branch.as_deref())
|
.bind(metadata.git_branch.as_deref())
|
||||||
.bind(metadata.git_origin_url.as_deref())
|
.bind(metadata.git_origin_url.as_deref())
|
||||||
@@ -666,14 +723,14 @@ ON CONFLICT(id) DO NOTHING
|
|||||||
Ok(result.rows_affected() > 0)
|
Ok(result.rows_affected() > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the SQLite-owned pinned state without changing other thread metadata.
|
/// Update the SQLite-owned section without changing other thread metadata.
|
||||||
pub async fn update_thread_pin(
|
pub async fn update_thread_section(
|
||||||
&self,
|
&self,
|
||||||
thread_id: ThreadId,
|
thread_id: ThreadId,
|
||||||
is_pinned: bool,
|
section: Option<&str>,
|
||||||
) -> anyhow::Result<bool> {
|
) -> anyhow::Result<bool> {
|
||||||
let result = sqlx::query("UPDATE threads SET is_pinned = ? WHERE id = ?")
|
let result = sqlx::query("UPDATE threads SET thread_section_id = ? WHERE id = ?")
|
||||||
.bind(is_pinned)
|
.bind(section)
|
||||||
.bind(thread_id.to_string())
|
.bind(thread_id.to_string())
|
||||||
.execute(self.pool.as_ref())
|
.execute(self.pool.as_ref())
|
||||||
.await?;
|
.await?;
|
||||||
@@ -853,7 +910,7 @@ INSERT INTO threads (
|
|||||||
first_user_message,
|
first_user_message,
|
||||||
archived,
|
archived,
|
||||||
archived_at,
|
archived_at,
|
||||||
is_pinned,
|
thread_section_id,
|
||||||
git_sha,
|
git_sha,
|
||||||
git_branch,
|
git_branch,
|
||||||
git_origin_url,
|
git_origin_url,
|
||||||
@@ -929,7 +986,7 @@ ON CONFLICT(id) DO UPDATE SET
|
|||||||
.bind(metadata.first_user_message.as_deref().unwrap_or_default())
|
.bind(metadata.first_user_message.as_deref().unwrap_or_default())
|
||||||
.bind(metadata.archived_at.is_some())
|
.bind(metadata.archived_at.is_some())
|
||||||
.bind(metadata.archived_at.map(datetime_to_epoch_seconds))
|
.bind(metadata.archived_at.map(datetime_to_epoch_seconds))
|
||||||
.bind(metadata.is_pinned)
|
.bind(metadata.section.as_ref().map(|section| section.id.as_str()))
|
||||||
.bind(metadata.git_sha.as_deref())
|
.bind(metadata.git_sha.as_deref())
|
||||||
.bind(metadata.git_branch.as_deref())
|
.bind(metadata.git_branch.as_deref())
|
||||||
.bind(metadata.git_origin_url.as_deref())
|
.bind(metadata.git_origin_url.as_deref())
|
||||||
@@ -1216,7 +1273,12 @@ SELECT
|
|||||||
threads.tokens_used,
|
threads.tokens_used,
|
||||||
threads.first_user_message,
|
threads.first_user_message,
|
||||||
threads.archived_at,
|
threads.archived_at,
|
||||||
threads.is_pinned,
|
threads.thread_section_id AS section,
|
||||||
|
(
|
||||||
|
SELECT thread_sections.name
|
||||||
|
FROM thread_sections
|
||||||
|
WHERE thread_sections.id = threads.thread_section_id
|
||||||
|
) AS section_name,
|
||||||
threads.git_sha,
|
threads.git_sha,
|
||||||
threads.git_branch,
|
threads.git_branch,
|
||||||
threads.git_origin_url
|
threads.git_origin_url
|
||||||
@@ -1249,7 +1311,7 @@ pub struct ThreadFilterOptions<'a> {
|
|||||||
pub allowed_sources: &'a [String],
|
pub allowed_sources: &'a [String],
|
||||||
pub model_providers: Option<&'a [String]>,
|
pub model_providers: Option<&'a [String]>,
|
||||||
pub cwd_filters: Option<&'a [PathBuf]>,
|
pub cwd_filters: Option<&'a [PathBuf]>,
|
||||||
pub is_pinned: Option<bool>,
|
pub section: Option<Option<&'a str>>,
|
||||||
pub anchor: Option<&'a crate::Anchor>,
|
pub anchor: Option<&'a crate::Anchor>,
|
||||||
pub sort_key: SortKey,
|
pub sort_key: SortKey,
|
||||||
pub sort_direction: SortDirection,
|
pub sort_direction: SortDirection,
|
||||||
@@ -1280,7 +1342,7 @@ fn push_thread_filters_with_preview<'a>(
|
|||||||
allowed_sources,
|
allowed_sources,
|
||||||
model_providers,
|
model_providers,
|
||||||
cwd_filters,
|
cwd_filters,
|
||||||
is_pinned,
|
section,
|
||||||
anchor,
|
anchor,
|
||||||
sort_key,
|
sort_key,
|
||||||
sort_direction,
|
sort_direction,
|
||||||
@@ -1295,9 +1357,15 @@ fn push_thread_filters_with_preview<'a>(
|
|||||||
if !include_empty_preview {
|
if !include_empty_preview {
|
||||||
builder.push(" AND threads.preview <> ''");
|
builder.push(" AND threads.preview <> ''");
|
||||||
}
|
}
|
||||||
if let Some(is_pinned) = is_pinned {
|
match section {
|
||||||
builder.push(" AND threads.is_pinned = ");
|
Some(Some(section)) => {
|
||||||
builder.push_bind(is_pinned);
|
builder.push(" AND threads.thread_section_id = ");
|
||||||
|
builder.push_bind(section);
|
||||||
|
}
|
||||||
|
Some(None) => {
|
||||||
|
builder.push(" AND threads.thread_section_id IS NULL");
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
if !allowed_sources.is_empty() {
|
if !allowed_sources.is_empty() {
|
||||||
builder.push(" AND threads.source IN (");
|
builder.push(" AND threads.source IN (");
|
||||||
@@ -1512,7 +1580,121 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn thread_pin_updates_round_trip_and_survive_rollout_reconciliation() {
|
async fn thread_sections_paginate_and_require_registered_identities() {
|
||||||
|
let codex_home = unique_temp_dir();
|
||||||
|
let runtime = StateRuntime::init(
|
||||||
|
crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()),
|
||||||
|
"test-provider".to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("state db should initialize");
|
||||||
|
let before_pinned = crate::ThreadSection {
|
||||||
|
id: "01984de2-8f74-7c91-a3b2-5c5e937cf317".to_string(),
|
||||||
|
name: "Before pinned".to_string(),
|
||||||
|
};
|
||||||
|
let pinned = crate::ThreadSection {
|
||||||
|
id: crate::PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
name: crate::PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
};
|
||||||
|
let after_pinned = crate::ThreadSection {
|
||||||
|
id: "01984de2-8f74-7c91-a3b2-5c5e937cf319".to_string(),
|
||||||
|
name: "After pinned".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for section in [&before_pinned, &after_pinned] {
|
||||||
|
sqlx::query("INSERT INTO thread_sections (id, name) VALUES (?, ?)")
|
||||||
|
.bind(§ion.id)
|
||||||
|
.bind(§ion.name)
|
||||||
|
.execute(runtime.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.expect("custom test sections should be explicitly registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
runtime
|
||||||
|
.get_thread_section(&pinned.id)
|
||||||
|
.await
|
||||||
|
.expect("built-in section should load"),
|
||||||
|
Some(pinned.clone())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
runtime
|
||||||
|
.get_thread_section("01984de2-8f74-7c91-a3b2-5c5e937cf320")
|
||||||
|
.await
|
||||||
|
.expect("missing section lookup should succeed"),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
runtime
|
||||||
|
.list_thread_sections(/*cursor*/ None, /*limit*/ 1)
|
||||||
|
.await
|
||||||
|
.expect("first section page should load"),
|
||||||
|
crate::ThreadSectionsPage {
|
||||||
|
sections: vec![before_pinned.clone()],
|
||||||
|
next_cursor: Some(before_pinned.id.clone()),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
runtime
|
||||||
|
.list_thread_sections(Some(&before_pinned.id), /*limit*/ 1)
|
||||||
|
.await
|
||||||
|
.expect("pinned section page should load"),
|
||||||
|
crate::ThreadSectionsPage {
|
||||||
|
sections: vec![pinned.clone()],
|
||||||
|
next_cursor: Some(pinned.id.clone()),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
runtime
|
||||||
|
.list_thread_sections(Some(&pinned.id), /*limit*/ 1)
|
||||||
|
.await
|
||||||
|
.expect("final section page should load"),
|
||||||
|
crate::ThreadSectionsPage {
|
||||||
|
sections: vec![after_pinned],
|
||||||
|
next_cursor: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let thread_id = ThreadId::new();
|
||||||
|
let mut metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone());
|
||||||
|
metadata.section = Some(before_pinned.clone());
|
||||||
|
runtime
|
||||||
|
.upsert_thread(&metadata)
|
||||||
|
.await
|
||||||
|
.expect("registered section should be accepted");
|
||||||
|
assert_eq!(
|
||||||
|
runtime
|
||||||
|
.get_thread(thread_id)
|
||||||
|
.await
|
||||||
|
.expect("sectioned thread should load")
|
||||||
|
.expect("sectioned thread should exist")
|
||||||
|
.section,
|
||||||
|
Some(before_pinned.clone())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
runtime
|
||||||
|
.update_thread_section(
|
||||||
|
thread_id,
|
||||||
|
/*section*/ Some("01984de2-8f74-7c91-a3b2-5c5e937cf320"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err(),
|
||||||
|
"thread sections must be explicitly registered before assignment"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
runtime
|
||||||
|
.get_thread(thread_id)
|
||||||
|
.await
|
||||||
|
.expect("thread should survive rejected section assignment")
|
||||||
|
.expect("thread should still exist")
|
||||||
|
.section,
|
||||||
|
Some(before_pinned)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_section_updates_round_trip_and_survive_rollout_reconciliation() {
|
||||||
let codex_home = unique_temp_dir();
|
let codex_home = unique_temp_dir();
|
||||||
let runtime = StateRuntime::init(
|
let runtime = StateRuntime::init(
|
||||||
crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()),
|
crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()),
|
||||||
@@ -1526,67 +1708,83 @@ mod tests {
|
|||||||
.upsert_thread(&metadata)
|
.upsert_thread(&metadata)
|
||||||
.await
|
.await
|
||||||
.expect("thread insert should succeed");
|
.expect("thread insert should succeed");
|
||||||
assert!(
|
assert_eq!(
|
||||||
!runtime
|
runtime
|
||||||
.get_thread(thread_id)
|
.get_thread(thread_id)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.is_pinned
|
.section,
|
||||||
|
None
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
runtime
|
runtime
|
||||||
.update_thread_pin(thread_id, /*is_pinned*/ true)
|
.update_thread_section(
|
||||||
|
thread_id,
|
||||||
|
/*section*/ Some(crate::PINNED_THREAD_SECTION_ID),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
assert!(
|
assert_eq!(
|
||||||
runtime
|
runtime
|
||||||
.get_thread(thread_id)
|
.get_thread(thread_id)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.is_pinned
|
.section,
|
||||||
|
Some(crate::ThreadSection {
|
||||||
|
id: crate::PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
name: crate::PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
runtime
|
runtime
|
||||||
.upsert_thread(&metadata)
|
.upsert_thread(&metadata)
|
||||||
.await
|
.await
|
||||||
.expect("stale rollout metadata should reconcile");
|
.expect("stale rollout metadata should reconcile");
|
||||||
assert!(
|
assert_eq!(
|
||||||
runtime
|
runtime
|
||||||
.get_thread(thread_id)
|
.get_thread(thread_id)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.is_pinned
|
.section,
|
||||||
|
Some(crate::ThreadSection {
|
||||||
|
id: crate::PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
name: crate::PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
runtime
|
runtime
|
||||||
.update_thread_pin(thread_id, /*is_pinned*/ false)
|
.update_thread_section(thread_id, /*section*/ None)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
assert!(
|
assert_eq!(
|
||||||
!runtime
|
runtime
|
||||||
.get_thread(thread_id)
|
.get_thread(thread_id)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.is_pinned
|
.section,
|
||||||
|
None
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!runtime
|
!runtime
|
||||||
.update_thread_pin(ThreadId::new(), /*is_pinned*/ true)
|
.update_thread_section(
|
||||||
|
ThreadId::new(),
|
||||||
|
/*section*/ Some(crate::PINNED_THREAD_SECTION_ID),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn list_threads_filters_pins_before_recency_pagination_and_uses_index() {
|
async fn list_threads_filters_sections_before_recency_pagination_and_uses_index() {
|
||||||
let codex_home = unique_temp_dir();
|
let codex_home = unique_temp_dir();
|
||||||
let runtime = StateRuntime::init(
|
let runtime = StateRuntime::init(
|
||||||
crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()),
|
crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()),
|
||||||
@@ -1601,40 +1799,63 @@ mod tests {
|
|||||||
let oldest_unpinned =
|
let oldest_unpinned =
|
||||||
ThreadId::from_string("00000000-0000-0000-0000-000000000044").unwrap();
|
ThreadId::from_string("00000000-0000-0000-0000-000000000044").unwrap();
|
||||||
|
|
||||||
for (thread_id, recency_at, is_pinned) in [
|
for (thread_id, recency_at, section) in [
|
||||||
(oldest_pinned, 1_700_000_001, true),
|
(
|
||||||
(newest_unpinned, 1_700_000_003, false),
|
oldest_pinned,
|
||||||
(newest_pinned, 1_700_000_002, true),
|
1_700_000_001,
|
||||||
(oldest_unpinned, 1_700_000_000, false),
|
Some(crate::PINNED_THREAD_SECTION_ID),
|
||||||
|
),
|
||||||
|
(newest_unpinned, 1_700_000_003, None),
|
||||||
|
(
|
||||||
|
newest_pinned,
|
||||||
|
1_700_000_002,
|
||||||
|
Some(crate::PINNED_THREAD_SECTION_ID),
|
||||||
|
),
|
||||||
|
(oldest_unpinned, 1_700_000_000, None),
|
||||||
] {
|
] {
|
||||||
let mut metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone());
|
let mut metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone());
|
||||||
metadata.recency_at = DateTime::<Utc>::from_timestamp(recency_at, 0).unwrap();
|
metadata.recency_at = DateTime::<Utc>::from_timestamp(recency_at, 0).unwrap();
|
||||||
metadata.is_pinned = is_pinned;
|
metadata.section = section.map(|id| crate::ThreadSection {
|
||||||
|
id: id.to_string(),
|
||||||
|
name: crate::PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
});
|
||||||
runtime.upsert_thread(&metadata).await.unwrap();
|
runtime.upsert_thread(&metadata).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let filters = |anchor, is_pinned| ThreadFilterOptions {
|
let filters = |anchor, section| ThreadFilterOptions {
|
||||||
archived_only: false,
|
archived_only: false,
|
||||||
allowed_sources: &[],
|
allowed_sources: &[],
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: Some(is_pinned),
|
section,
|
||||||
anchor,
|
anchor,
|
||||||
sort_key: SortKey::RecencyAt,
|
sort_key: SortKey::RecencyAt,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
};
|
};
|
||||||
let first_page = runtime
|
let first_page = runtime
|
||||||
.list_threads(/*page_size*/ 1, filters(None, true))
|
.list_threads(
|
||||||
|
/*page_size*/ 1,
|
||||||
|
filters(None, Some(Some(crate::PINNED_THREAD_SECTION_ID))),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(first_page.items.len(), 1);
|
assert_eq!(first_page.items.len(), 1);
|
||||||
assert_eq!(first_page.items[0].id, newest_pinned);
|
assert_eq!(first_page.items[0].id, newest_pinned);
|
||||||
assert!(first_page.items[0].is_pinned);
|
assert_eq!(
|
||||||
|
first_page.items[0].section,
|
||||||
|
Some(crate::ThreadSection {
|
||||||
|
id: crate::PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
name: crate::PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
let second_page = runtime
|
let second_page = runtime
|
||||||
.list_threads(
|
.list_threads(
|
||||||
/*page_size*/ 1,
|
/*page_size*/ 1,
|
||||||
filters(first_page.next_anchor.as_ref(), true),
|
filters(
|
||||||
|
first_page.next_anchor.as_ref(),
|
||||||
|
Some(Some(crate::PINNED_THREAD_SECTION_ID)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -1642,12 +1863,12 @@ mod tests {
|
|||||||
assert_eq!(second_page.items[0].id, oldest_pinned);
|
assert_eq!(second_page.items[0].id, oldest_pinned);
|
||||||
assert_eq!(second_page.next_anchor, None);
|
assert_eq!(second_page.next_anchor, None);
|
||||||
|
|
||||||
let unpinned_page = runtime
|
let unsectioned_page = runtime
|
||||||
.list_threads(/*page_size*/ 10, filters(None, false))
|
.list_threads(/*page_size*/ 10, filters(None, Some(None)))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unpinned_page
|
unsectioned_page
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|thread| thread.id)
|
.map(|thread| thread.id)
|
||||||
@@ -1655,10 +1876,28 @@ mod tests {
|
|||||||
vec![newest_unpinned, oldest_unpinned]
|
vec![newest_unpinned, oldest_unpinned]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let all_sections_page = runtime
|
||||||
|
.list_threads(/*page_size*/ 10, filters(None, None))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
all_sections_page
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|thread| thread.id)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
newest_unpinned,
|
||||||
|
newest_pinned,
|
||||||
|
oldest_pinned,
|
||||||
|
oldest_unpinned,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
let mut builder = QueryBuilder::<Sqlite>::new("EXPLAIN QUERY PLAN ");
|
let mut builder = QueryBuilder::<Sqlite>::new("EXPLAIN QUERY PLAN ");
|
||||||
push_list_threads_query(
|
push_list_threads_query(
|
||||||
&mut builder,
|
&mut builder,
|
||||||
filters(None, true),
|
filters(None, Some(Some(crate::PINNED_THREAD_SECTION_ID))),
|
||||||
/*relation_filter*/ None,
|
/*relation_filter*/ None,
|
||||||
/*limit*/ 2,
|
/*limit*/ 2,
|
||||||
);
|
);
|
||||||
@@ -1673,14 +1912,14 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
plan_details
|
plan_details
|
||||||
.iter()
|
.iter()
|
||||||
.any(|detail| detail.contains("idx_threads_pinned_recency_at_ms")),
|
.any(|detail| detail.contains("idx_threads_section_recency_at_ms")),
|
||||||
"pinned listing did not use its selective recency index: {plan_details:?}"
|
"section listing did not use its selective recency index: {plan_details:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!plan_details
|
!plan_details
|
||||||
.iter()
|
.iter()
|
||||||
.any(|detail| detail.contains("TEMP B-TREE")),
|
.any(|detail| detail.contains("TEMP B-TREE")),
|
||||||
"pinned listing unexpectedly sorted outside its index: {plan_details:?}"
|
"section listing unexpectedly sorted outside its index: {plan_details:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1868,7 +2107,7 @@ mod tests {
|
|||||||
allowed_sources: &[],
|
allowed_sources: &[],
|
||||||
model_providers: Some(&model_providers),
|
model_providers: Some(&model_providers),
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor: Some(&anchor),
|
anchor: Some(&anchor),
|
||||||
sort_key: SortKey::UpdatedAt,
|
sort_key: SortKey::UpdatedAt,
|
||||||
sort_direction: SortDirection::Asc,
|
sort_direction: SortDirection::Asc,
|
||||||
@@ -1897,7 +2136,7 @@ mod tests {
|
|||||||
allowed_sources: &[],
|
allowed_sources: &[],
|
||||||
model_providers: Some(&model_providers),
|
model_providers: Some(&model_providers),
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor: page.next_anchor.as_ref(),
|
anchor: page.next_anchor.as_ref(),
|
||||||
sort_key: SortKey::UpdatedAt,
|
sort_key: SortKey::UpdatedAt,
|
||||||
sort_direction: SortDirection::Asc,
|
sort_direction: SortDirection::Asc,
|
||||||
@@ -1954,7 +2193,7 @@ mod tests {
|
|||||||
allowed_sources: &[],
|
allowed_sources: &[],
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: Some(cwd_filters.as_slice()),
|
cwd_filters: Some(cwd_filters.as_slice()),
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor: None,
|
anchor: None,
|
||||||
sort_key: SortKey::UpdatedAt,
|
sort_key: SortKey::UpdatedAt,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
@@ -1987,7 +2226,7 @@ mod tests {
|
|||||||
allowed_sources: &[],
|
allowed_sources: &[],
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: Some(cwd_filters.as_slice()),
|
cwd_filters: Some(cwd_filters.as_slice()),
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor: first_page.next_anchor.as_ref(),
|
anchor: first_page.next_anchor.as_ref(),
|
||||||
sort_key: SortKey::UpdatedAt,
|
sort_key: SortKey::UpdatedAt,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
@@ -2013,7 +2252,7 @@ mod tests {
|
|||||||
allowed_sources: &[],
|
allowed_sources: &[],
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: Some(&[]),
|
cwd_filters: Some(&[]),
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor: None,
|
anchor: None,
|
||||||
sort_key: SortKey::UpdatedAt,
|
sort_key: SortKey::UpdatedAt,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
@@ -2081,7 +2320,7 @@ mod tests {
|
|||||||
allowed_sources: &[],
|
allowed_sources: &[],
|
||||||
model_providers: Some(&model_providers),
|
model_providers: Some(&model_providers),
|
||||||
cwd_filters,
|
cwd_filters,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor,
|
anchor,
|
||||||
sort_key,
|
sort_key,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
@@ -2182,7 +2421,7 @@ mod tests {
|
|||||||
allowed_sources: &[],
|
allowed_sources: &[],
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor: None,
|
anchor: None,
|
||||||
sort_key: SortKey::CreatedAt,
|
sort_key: SortKey::CreatedAt,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
@@ -2211,7 +2450,7 @@ mod tests {
|
|||||||
allowed_sources: &[],
|
allowed_sources: &[],
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor,
|
anchor,
|
||||||
sort_key: SortKey::CreatedAt,
|
sort_key: SortKey::CreatedAt,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
@@ -2895,7 +3134,7 @@ mod tests {
|
|||||||
allowed_sources: &[],
|
allowed_sources: &[],
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor: None,
|
anchor: None,
|
||||||
sort_key: SortKey::RecencyAt,
|
sort_key: SortKey::RecencyAt,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
@@ -2928,7 +3167,7 @@ mod tests {
|
|||||||
allowed_sources: &[],
|
allowed_sources: &[],
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor: first_page.next_anchor.as_ref(),
|
anchor: first_page.next_anchor.as_ref(),
|
||||||
sort_key: SortKey::RecencyAt,
|
sort_key: SortKey::RecencyAt,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
@@ -2961,7 +3200,7 @@ mod tests {
|
|||||||
allowed_sources: &[],
|
allowed_sources: &[],
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
anchor: second_page.next_anchor.as_ref(),
|
anchor: second_page.next_anchor.as_ref(),
|
||||||
sort_key: SortKey::RecencyAt,
|
sort_key: SortKey::RecencyAt,
|
||||||
sort_direction: SortDirection::Desc,
|
sort_direction: SortDirection::Desc,
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ mod tests {
|
|||||||
.update_thread_metadata(UpdateThreadMetadataParams {
|
.update_thread_metadata(UpdateThreadMetadataParams {
|
||||||
thread_id: grandchild_thread_id,
|
thread_id: grandchild_thread_id,
|
||||||
patch: ThreadMetadataPatch {
|
patch: ThreadMetadataPatch {
|
||||||
is_pinned: Some(true),
|
section: Some(Some(codex_state::PINNED_THREAD_SECTION_ID.to_string())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
include_archived: false,
|
include_archived: false,
|
||||||
@@ -170,7 +170,7 @@ mod tests {
|
|||||||
allowed_sources: Vec::new(),
|
allowed_sources: Vec::new(),
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
archived: false,
|
archived: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: Some(ThreadRelationFilter::DirectChildrenOf(parent_thread_id)),
|
relation_filter: Some(ThreadRelationFilter::DirectChildrenOf(parent_thread_id)),
|
||||||
@@ -198,7 +198,7 @@ mod tests {
|
|||||||
allowed_sources: Vec::new(),
|
allowed_sources: Vec::new(),
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
archived: false,
|
archived: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: Some(ThreadRelationFilter::DescendantsOf(parent_thread_id)),
|
relation_filter: Some(ThreadRelationFilter::DescendantsOf(parent_thread_id)),
|
||||||
@@ -226,7 +226,7 @@ mod tests {
|
|||||||
allowed_sources: Vec::new(),
|
allowed_sources: Vec::new(),
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: Some(true),
|
section: Some(Some(codex_state::PINNED_THREAD_SECTION_ID.to_string())),
|
||||||
archived: false,
|
archived: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: Some(ThreadRelationFilter::DescendantsOf(parent_thread_id)),
|
relation_filter: Some(ThreadRelationFilter::DescendantsOf(parent_thread_id)),
|
||||||
@@ -243,6 +243,34 @@ mod tests {
|
|||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
vec![grandchild_thread_id]
|
vec![grandchild_thread_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let page = ThreadStore::list_threads(
|
||||||
|
&store,
|
||||||
|
ListThreadsParams {
|
||||||
|
page_size: 10,
|
||||||
|
cursor: None,
|
||||||
|
sort_key: ThreadSortKey::CreatedAt,
|
||||||
|
sort_direction: SortDirection::Desc,
|
||||||
|
allowed_sources: Vec::new(),
|
||||||
|
model_providers: None,
|
||||||
|
cwd_filters: None,
|
||||||
|
section: Some(None),
|
||||||
|
archived: false,
|
||||||
|
search_term: None,
|
||||||
|
relation_filter: Some(ThreadRelationFilter::DescendantsOf(parent_thread_id)),
|
||||||
|
use_state_db_only: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("list unsectioned descendant threads");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
page.items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| item.thread_id)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![child_thread_id]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -772,8 +800,10 @@ impl ThreadStore for InMemoryThreadStore {
|
|||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
if let Some(is_pinned) = params.is_pinned {
|
if let Some(section) = params.section.as_ref() {
|
||||||
page.items.retain(|thread| thread.is_pinned == is_pinned);
|
page.items.retain(|thread| {
|
||||||
|
thread.section.as_ref().map(|section| section.id.as_str()) == section.as_deref()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Ok(page)
|
Ok(page)
|
||||||
})
|
})
|
||||||
@@ -858,9 +888,16 @@ fn stored_thread_from_state(
|
|||||||
.and_then(|metadata| metadata.advance_recency_at.or(metadata.updated_at))
|
.and_then(|metadata| metadata.advance_recency_at.or(metadata.updated_at))
|
||||||
.unwrap_or_else(Utc::now),
|
.unwrap_or_else(Utc::now),
|
||||||
archived_at: None,
|
archived_at: None,
|
||||||
is_pinned: metadata
|
section: metadata
|
||||||
.and_then(|metadata| metadata.is_pinned)
|
.and_then(|metadata| metadata.section.clone().flatten())
|
||||||
.unwrap_or(false),
|
.map(|id| codex_state::ThreadSection {
|
||||||
|
name: if id == codex_state::PINNED_THREAD_SECTION_ID {
|
||||||
|
codex_state::PINNED_THREAD_SECTION_NAME.to_string()
|
||||||
|
} else {
|
||||||
|
id.clone()
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
}),
|
||||||
cwd: metadata
|
cwd: metadata
|
||||||
.and_then(|metadata| metadata.cwd.clone())
|
.and_then(|metadata| metadata.cwd.clone())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ mod tests {
|
|||||||
allowed_sources: Vec::new(),
|
allowed_sources: Vec::new(),
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
archived: true,
|
archived: true,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ pub(super) fn stored_thread_from_rollout_item(
|
|||||||
updated_at,
|
updated_at,
|
||||||
recency_at,
|
recency_at,
|
||||||
archived_at,
|
archived_at,
|
||||||
is_pinned: item.is_pinned,
|
section: item.section,
|
||||||
cwd: item.cwd.unwrap_or_default(),
|
cwd: item.cwd.unwrap_or_default(),
|
||||||
cli_version: item.cli_version.unwrap_or_default(),
|
cli_version: item.cli_version.unwrap_or_default(),
|
||||||
source,
|
source,
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ pub(super) async fn list_rollout_threads(
|
|||||||
sort_key: codex_rollout::ThreadSortKey,
|
sort_key: codex_rollout::ThreadSortKey,
|
||||||
sort_direction: codex_rollout::SortDirection,
|
sort_direction: codex_rollout::SortDirection,
|
||||||
) -> ThreadStoreResult<codex_rollout::ThreadsPage> {
|
) -> ThreadStoreResult<codex_rollout::ThreadsPage> {
|
||||||
if params.relation_filter.is_some() || params.is_pinned.is_some() {
|
if params.relation_filter.is_some() || params.section.is_some() {
|
||||||
let relation_filter = params
|
let relation_filter = params
|
||||||
.relation_filter
|
.relation_filter
|
||||||
.map(|relation_filter| match relation_filter {
|
.map(|relation_filter| match relation_filter {
|
||||||
@@ -120,7 +120,7 @@ pub(super) async fn list_rollout_threads(
|
|||||||
params.cwd_filters.as_deref(),
|
params.cwd_filters.as_deref(),
|
||||||
relation_filter,
|
relation_filter,
|
||||||
params.archived,
|
params.archived,
|
||||||
params.is_pinned,
|
params.section.as_ref().map(Option::as_deref),
|
||||||
params.search_term.as_deref(),
|
params.search_term.as_deref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -240,7 +240,7 @@ mod tests {
|
|||||||
allowed_sources: Vec::new(),
|
allowed_sources: Vec::new(),
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
archived: false,
|
archived: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
@@ -301,7 +301,7 @@ mod tests {
|
|||||||
allowed_sources: Vec::new(),
|
allowed_sources: Vec::new(),
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
archived: false,
|
archived: false,
|
||||||
search_term: Some("needle".to_string()),
|
search_term: Some("needle".to_string()),
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
@@ -374,7 +374,7 @@ mod tests {
|
|||||||
allowed_sources: Vec::new(),
|
allowed_sources: Vec::new(),
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
archived: false,
|
archived: false,
|
||||||
search_term: Some("canonical".to_string()),
|
search_term: Some("canonical".to_string()),
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
@@ -411,7 +411,7 @@ mod tests {
|
|||||||
allowed_sources: Vec::new(),
|
allowed_sources: Vec::new(),
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
archived: false,
|
archived: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
@@ -428,7 +428,7 @@ mod tests {
|
|||||||
allowed_sources: Vec::new(),
|
allowed_sources: Vec::new(),
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
archived: true,
|
archived: true,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
@@ -481,7 +481,7 @@ mod tests {
|
|||||||
allowed_sources: vec![SessionSource::Cli],
|
allowed_sources: vec![SessionSource::Cli],
|
||||||
model_providers: Some(vec!["test-provider".to_string()]),
|
model_providers: Some(vec!["test-provider".to_string()]),
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
archived: false,
|
archived: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
@@ -519,7 +519,7 @@ mod tests {
|
|||||||
allowed_sources: Vec::new(),
|
allowed_sources: Vec::new(),
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
archived: false,
|
archived: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ pub(super) async fn read_thread(
|
|||||||
&& !rollout_thread.preview.is_empty()
|
&& !rollout_thread.preview.is_empty()
|
||||||
{
|
{
|
||||||
rollout_thread.recency_at = thread.recency_at;
|
rollout_thread.recency_at = thread.recency_at;
|
||||||
rollout_thread.is_pinned = thread.is_pinned;
|
rollout_thread.section = thread.section;
|
||||||
if thread.name.is_some() {
|
if thread.name.is_some() {
|
||||||
rollout_thread.name = thread.name;
|
rollout_thread.name = thread.name;
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ pub(super) async fn read_thread_by_rollout_path(
|
|||||||
thread = stored_thread_from_sqlite_metadata(store, metadata).await?;
|
thread = stored_thread_from_sqlite_metadata(store, metadata).await?;
|
||||||
} else {
|
} else {
|
||||||
thread.recency_at = metadata.recency_at;
|
thread.recency_at = metadata.recency_at;
|
||||||
thread.is_pinned = metadata.is_pinned;
|
thread.section = metadata.section;
|
||||||
let (fallback_sha, fallback_branch, fallback_origin_url) = match thread.git_info.take()
|
let (fallback_sha, fallback_branch, fallback_origin_url) = match thread.git_info.take()
|
||||||
{
|
{
|
||||||
Some(info) => (
|
Some(info) => (
|
||||||
@@ -383,7 +383,7 @@ async fn stored_thread_from_sqlite_metadata(
|
|||||||
updated_at: metadata.updated_at,
|
updated_at: metadata.updated_at,
|
||||||
recency_at: metadata.recency_at,
|
recency_at: metadata.recency_at,
|
||||||
archived_at: metadata.archived_at,
|
archived_at: metadata.archived_at,
|
||||||
is_pinned: metadata.is_pinned,
|
section: metadata.section,
|
||||||
cwd: metadata.cwd,
|
cwd: metadata.cwd,
|
||||||
cli_version: metadata.cli_version,
|
cli_version: metadata.cli_version,
|
||||||
source: parse_session_source(&metadata.source),
|
source: parse_session_source(&metadata.source),
|
||||||
@@ -479,7 +479,7 @@ fn stored_thread_from_meta_line(
|
|||||||
updated_at,
|
updated_at,
|
||||||
recency_at: updated_at,
|
recency_at: updated_at,
|
||||||
archived_at: archived.then_some(updated_at),
|
archived_at: archived.then_some(updated_at),
|
||||||
is_pinned: false,
|
section: None,
|
||||||
cwd: meta_line.meta.cwd,
|
cwd: meta_line.meta.cwd,
|
||||||
cli_version: meta_line.meta.cli_version,
|
cli_version: meta_line.meta.cli_version,
|
||||||
source: meta_line.meta.source,
|
source: meta_line.meta.source,
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ pub(super) async fn search_threads(
|
|||||||
allowed_sources: params.allowed_sources.clone(),
|
allowed_sources: params.allowed_sources.clone(),
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
archived: params.archived,
|
archived: params.archived,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ async fn split_homes_support_backfill_listing_and_paginated_history() {
|
|||||||
archived: false,
|
archived: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
is_pinned: None,
|
section: None,
|
||||||
use_state_db_only: true,
|
use_state_db_only: true,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -165,7 +165,10 @@ mod tests {
|
|||||||
builder.cli_version = Some("test_version".to_string());
|
builder.cli_version = Some("test_version".to_string());
|
||||||
let mut metadata = builder.build(config.default_model_provider_id.as_str());
|
let mut metadata = builder.build(config.default_model_provider_id.as_str());
|
||||||
metadata.archived_at = Some(metadata.updated_at);
|
metadata.archived_at = Some(metadata.updated_at);
|
||||||
metadata.is_pinned = true;
|
metadata.section = Some(codex_state::ThreadSection {
|
||||||
|
id: codex_state::PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
});
|
||||||
runtime
|
runtime
|
||||||
.upsert_thread(&metadata)
|
.upsert_thread(&metadata)
|
||||||
.await
|
.await
|
||||||
@@ -175,7 +178,7 @@ mod tests {
|
|||||||
.unarchive_thread(ArchiveThreadParams { thread_id })
|
.unarchive_thread(ArchiveThreadParams { thread_id })
|
||||||
.await
|
.await
|
||||||
.expect("unarchive thread");
|
.expect("unarchive thread");
|
||||||
assert!(unarchived.is_pinned);
|
assert_eq!(unarchived.section, metadata.section);
|
||||||
|
|
||||||
let restored_path = home
|
let restored_path = home
|
||||||
.path()
|
.path()
|
||||||
@@ -189,6 +192,6 @@ mod tests {
|
|||||||
assert_eq!(updated.rollout_path, restored_path);
|
assert_eq!(updated.rollout_path, restored_path);
|
||||||
assert_eq!(updated.archived_at, None);
|
assert_eq!(updated.archived_at, None);
|
||||||
assert_eq!(updated.recency_at, metadata.recency_at);
|
assert_eq!(updated.recency_at, metadata.recency_at);
|
||||||
assert!(updated.is_pinned);
|
assert_eq!(updated.section, metadata.section);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ async fn apply_metadata_update(
|
|||||||
rollout_path = Some(resolved.path);
|
rollout_path = Some(resolved.path);
|
||||||
}
|
}
|
||||||
if existing.is_none()
|
if existing.is_none()
|
||||||
&& patch.is_pinned.is_some()
|
&& patch.section.is_some()
|
||||||
&& let Some(path) = rollout_path.as_deref()
|
&& let Some(path) = rollout_path.as_deref()
|
||||||
&& let Some(existing_rollout_path) =
|
&& let Some(existing_rollout_path) =
|
||||||
codex_rollout::existing_rollout_path(path).await
|
codex_rollout::existing_rollout_path(path).await
|
||||||
@@ -389,8 +389,23 @@ async fn apply_metadata_update(
|
|||||||
if let Some(first_user_message) = patch.first_user_message {
|
if let Some(first_user_message) = patch.first_user_message {
|
||||||
metadata.first_user_message = Some(first_user_message);
|
metadata.first_user_message = Some(first_user_message);
|
||||||
}
|
}
|
||||||
if let Some(is_pinned) = patch.is_pinned {
|
if let Some(section) = patch.section.clone() {
|
||||||
metadata.is_pinned = is_pinned;
|
metadata.section = match section {
|
||||||
|
Some(section_id) => Some(
|
||||||
|
state_db
|
||||||
|
.get_thread_section(§ion_id)
|
||||||
|
.await
|
||||||
|
.map_err(|err| ThreadStoreError::Internal {
|
||||||
|
message: format!(
|
||||||
|
"failed to read section {section_id} for thread {thread_id}: {err}"
|
||||||
|
),
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| ThreadStoreError::InvalidRequest {
|
||||||
|
message: format!("thread section not found: {section_id}"),
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if let Some(git_info) = patch.git_info {
|
if let Some(git_info) = patch.git_info {
|
||||||
let existing_git_info = git_info_from_parts(
|
let existing_git_info = git_info_from_parts(
|
||||||
@@ -409,19 +424,17 @@ async fn apply_metadata_update(
|
|||||||
.map_err(|err| ThreadStoreError::Internal {
|
.map_err(|err| ThreadStoreError::Internal {
|
||||||
message: format!("failed to update thread metadata for {thread_id}: {err}"),
|
message: format!("failed to update thread metadata for {thread_id}: {err}"),
|
||||||
})?;
|
})?;
|
||||||
if let Some(is_pinned) = patch.is_pinned {
|
if let Some(section) = patch.section {
|
||||||
let updated = state_db
|
let updated = state_db
|
||||||
.update_thread_pin(thread_id, is_pinned)
|
.update_thread_section(thread_id, section.as_deref())
|
||||||
.await
|
.await
|
||||||
.map_err(|err| ThreadStoreError::Internal {
|
.map_err(|err| ThreadStoreError::Internal {
|
||||||
message: format!(
|
message: format!("failed to update section for thread {thread_id}: {err}"),
|
||||||
"failed to update pin state for thread {thread_id}: {err}"
|
|
||||||
),
|
|
||||||
})?;
|
})?;
|
||||||
if !updated {
|
if !updated {
|
||||||
return Err(ThreadStoreError::Internal {
|
return Err(ThreadStoreError::Internal {
|
||||||
message: format!(
|
message: format!(
|
||||||
"thread metadata unavailable before pin update: {thread_id}"
|
"thread metadata unavailable before section update: {thread_id}"
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -590,9 +603,9 @@ fn sqlite_write_failure_should_block(patch: &ThreadMetadataPatch) -> bool {
|
|||||||
// transcript-derived metadata, thread names, and memory-mode indexing were log-only. Keep that
|
// transcript-derived metadata, thread names, and memory-mode indexing were log-only. Keep that
|
||||||
// failure isolation so a corrupted optional state DB does not make JSONL transcript durability
|
// failure isolation so a corrupted optional state DB does not make JSONL transcript durability
|
||||||
// look broken. Explicit git-only updates still require SQLite because partial git patches need
|
// look broken. Explicit git-only updates still require SQLite because partial git patches need
|
||||||
// the existing SQLite value to preserve unspecified fields. User-selected pin state is
|
// the existing SQLite value to preserve unspecified fields. User-selected section state is
|
||||||
// SQLite-only, so losing its write must also fail the explicit metadata update.
|
// SQLite-only, so losing its write must also fail the explicit metadata update.
|
||||||
patch.is_pinned.is_some() || (patch.git_info.is_some() && !has_observed_metadata_facts(patch))
|
patch.section.is_some() || (patch.git_info.is_some() && !has_observed_metadata_facts(patch))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sqlite_write_error_is_best_effort(err: &ThreadStoreError) -> bool {
|
fn sqlite_write_error_is_best_effort(err: &ThreadStoreError) -> bool {
|
||||||
@@ -892,7 +905,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn pin_only_metadata_updates_persist_in_sqlite_without_changing_the_rollout() {
|
async fn section_only_metadata_updates_persist_in_sqlite_without_changing_the_rollout() {
|
||||||
let home = TempDir::new().expect("temp dir");
|
let home = TempDir::new().expect("temp dir");
|
||||||
let config = test_config(home.path());
|
let config = test_config(home.path());
|
||||||
let uuid = Uuid::from_u128(320);
|
let uuid = Uuid::from_u128(320);
|
||||||
@@ -912,7 +925,7 @@ mod tests {
|
|||||||
.update_thread_metadata(UpdateThreadMetadataParams {
|
.update_thread_metadata(UpdateThreadMetadataParams {
|
||||||
thread_id,
|
thread_id,
|
||||||
patch: ThreadMetadataPatch {
|
patch: ThreadMetadataPatch {
|
||||||
is_pinned: Some(true),
|
section: Some(Some(codex_state::PINNED_THREAD_SECTION_ID.to_string())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
include_archived: false,
|
include_archived: false,
|
||||||
@@ -920,13 +933,25 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("pin thread");
|
.expect("pin thread");
|
||||||
|
|
||||||
assert!(pinned.is_pinned);
|
assert_eq!(
|
||||||
|
pinned.section,
|
||||||
|
Some(codex_state::ThreadSection {
|
||||||
|
id: codex_state::PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
let pinned_metadata = runtime
|
let pinned_metadata = runtime
|
||||||
.get_thread(thread_id)
|
.get_thread(thread_id)
|
||||||
.await
|
.await
|
||||||
.expect("read pinned metadata")
|
.expect("read pinned metadata")
|
||||||
.expect("pinned metadata");
|
.expect("pinned metadata");
|
||||||
assert!(pinned_metadata.is_pinned);
|
assert_eq!(
|
||||||
|
pinned_metadata.section,
|
||||||
|
Some(codex_state::ThreadSection {
|
||||||
|
id: codex_state::PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
assert_eq!(pinned_metadata.preview.as_deref(), Some("Hello from user"));
|
assert_eq!(pinned_metadata.preview.as_deref(), Some("Hello from user"));
|
||||||
assert_eq!(pinned_metadata.source, "cli");
|
assert_eq!(pinned_metadata.source, "cli");
|
||||||
let pinned_page = store
|
let pinned_page = store
|
||||||
@@ -938,7 +963,7 @@ mod tests {
|
|||||||
allowed_sources: Vec::new(),
|
allowed_sources: Vec::new(),
|
||||||
model_providers: None,
|
model_providers: None,
|
||||||
cwd_filters: None,
|
cwd_filters: None,
|
||||||
is_pinned: Some(true),
|
section: Some(Some(codex_state::PINNED_THREAD_SECTION_ID.to_string())),
|
||||||
archived: false,
|
archived: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
@@ -962,7 +987,13 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("read pinned thread by rollout path");
|
.expect("read pinned thread by rollout path");
|
||||||
assert!(read_by_path.is_pinned);
|
assert_eq!(
|
||||||
|
read_by_path.section,
|
||||||
|
Some(codex_state::ThreadSection {
|
||||||
|
id: codex_state::PINNED_THREAD_SECTION_ID.to_string(),
|
||||||
|
name: codex_state::PINNED_THREAD_SECTION_NAME.to_string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::fs::read_to_string(&rollout_path).expect("read rollout"),
|
std::fs::read_to_string(&rollout_path).expect("read rollout"),
|
||||||
original_rollout
|
original_rollout
|
||||||
@@ -972,22 +1003,23 @@ mod tests {
|
|||||||
.update_thread_metadata(UpdateThreadMetadataParams {
|
.update_thread_metadata(UpdateThreadMetadataParams {
|
||||||
thread_id,
|
thread_id,
|
||||||
patch: ThreadMetadataPatch {
|
patch: ThreadMetadataPatch {
|
||||||
is_pinned: Some(false),
|
section: Some(None),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
include_archived: false,
|
include_archived: false,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("unpin thread");
|
.expect("clear thread section");
|
||||||
|
|
||||||
assert!(!unpinned.is_pinned);
|
assert_eq!(unpinned.section, None);
|
||||||
assert!(
|
assert_eq!(
|
||||||
!runtime
|
runtime
|
||||||
.get_thread(thread_id)
|
.get_thread(thread_id)
|
||||||
.await
|
.await
|
||||||
.expect("read unpinned metadata")
|
.expect("read cleared metadata")
|
||||||
.expect("unpinned metadata")
|
.expect("cleared metadata")
|
||||||
.is_pinned
|
.section,
|
||||||
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::fs::read_to_string(&rollout_path).expect("read rollout"),
|
std::fs::read_to_string(&rollout_path).expect("read rollout"),
|
||||||
@@ -1798,9 +1830,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sqlite_failures_block_for_explicit_pin_updates() {
|
fn sqlite_failures_block_for_explicit_section_updates() {
|
||||||
assert!(sqlite_write_failure_should_block(&ThreadMetadataPatch {
|
assert!(sqlite_write_failure_should_block(&ThreadMetadataPatch {
|
||||||
is_pinned: Some(false),
|
section: Some(None),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -2078,7 +2110,7 @@ mod tests {
|
|||||||
allowed_sources: Vec::new(),
|
allowed_sources: Vec::new(),
|
||||||
model_providers: Some(Vec::new()),
|
model_providers: Some(Vec::new()),
|
||||||
cwd_filters: Some(vec![workspace]),
|
cwd_filters: Some(vec![workspace]),
|
||||||
is_pinned: None,
|
section: None,
|
||||||
archived: false,
|
archived: false,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
relation_filter: None,
|
relation_filter: None,
|
||||||
|
|||||||
@@ -300,8 +300,9 @@ pub struct ListThreadsParams {
|
|||||||
/// Optional cwd filters. `None` means all working directories, while an empty vector matches no
|
/// Optional cwd filters. `None` means all working directories, while an empty vector matches no
|
||||||
/// threads.
|
/// threads.
|
||||||
pub cwd_filters: Option<Vec<PathBuf>>,
|
pub cwd_filters: Option<Vec<PathBuf>>,
|
||||||
/// Optional persisted pin-state filter.
|
/// Omit to include every section, set to `None` to match unsectioned
|
||||||
pub is_pinned: Option<bool>,
|
/// threads, or provide a section ID to match that section.
|
||||||
|
pub section: Option<Option<String>>,
|
||||||
/// Whether archived threads should be listed instead of active threads.
|
/// Whether archived threads should be listed instead of active threads.
|
||||||
pub archived: bool,
|
pub archived: bool,
|
||||||
/// Optional substring/full-text search term for thread title/preview.
|
/// Optional substring/full-text search term for thread title/preview.
|
||||||
@@ -564,8 +565,8 @@ pub struct StoredThread {
|
|||||||
pub recency_at: DateTime<Utc>,
|
pub recency_at: DateTime<Utc>,
|
||||||
/// Thread archive timestamp, if archived.
|
/// Thread archive timestamp, if archived.
|
||||||
pub archived_at: Option<DateTime<Utc>>,
|
pub archived_at: Option<DateTime<Utc>>,
|
||||||
/// Whether this thread has been pinned by the user.
|
/// The user-selected section for this thread, if any.
|
||||||
pub is_pinned: bool,
|
pub section: Option<codex_state::ThreadSection>,
|
||||||
/// Working directory captured for the thread.
|
/// Working directory captured for the thread.
|
||||||
pub cwd: PathBuf,
|
pub cwd: PathBuf,
|
||||||
/// CLI version captured for the thread.
|
/// CLI version captured for the thread.
|
||||||
@@ -722,8 +723,13 @@ pub struct ThreadMetadataPatch {
|
|||||||
pub token_usage: Option<TokenUsage>,
|
pub token_usage: Option<TokenUsage>,
|
||||||
/// First user message observed for this thread.
|
/// First user message observed for this thread.
|
||||||
pub first_user_message: Option<String>,
|
pub first_user_message: Option<String>,
|
||||||
/// Replacement user-selected thread pin state.
|
/// Replacement user-selected section, clear request, or no-op.
|
||||||
pub is_pinned: Option<bool>,
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "optional_option"
|
||||||
|
)]
|
||||||
|
pub section: ClearableField<String>,
|
||||||
/// Git metadata patch.
|
/// Git metadata patch.
|
||||||
pub git_info: Option<GitInfoPatch>,
|
pub git_info: Option<GitInfoPatch>,
|
||||||
/// Thread memory behavior.
|
/// Thread memory behavior.
|
||||||
@@ -800,8 +806,8 @@ impl ThreadMetadataPatch {
|
|||||||
if next.first_user_message.is_some() {
|
if next.first_user_message.is_some() {
|
||||||
self.first_user_message = next.first_user_message;
|
self.first_user_message = next.first_user_message;
|
||||||
}
|
}
|
||||||
if next.is_pinned.is_some() {
|
if next.section.is_some() {
|
||||||
self.is_pinned = next.is_pinned;
|
self.section = next.section;
|
||||||
}
|
}
|
||||||
if let Some(git_info) = next.git_info {
|
if let Some(git_info) = next.git_info {
|
||||||
self.git_info
|
self.git_info
|
||||||
@@ -835,7 +841,7 @@ impl ThreadMetadataPatch {
|
|||||||
&& self.permission_profile.is_none()
|
&& self.permission_profile.is_none()
|
||||||
&& self.token_usage.is_none()
|
&& self.token_usage.is_none()
|
||||||
&& self.first_user_message.is_none()
|
&& self.first_user_message.is_none()
|
||||||
&& self.is_pinned.is_none()
|
&& self.section.is_none()
|
||||||
&& self.git_info.is_none()
|
&& self.git_info.is_none()
|
||||||
&& self.memory_mode.is_none()
|
&& self.memory_mode.is_none()
|
||||||
}
|
}
|
||||||
@@ -977,7 +983,7 @@ mod tests {
|
|||||||
let mut current = ThreadMetadataPatch {
|
let mut current = ThreadMetadataPatch {
|
||||||
name: Some(Some("old name".to_string())),
|
name: Some(Some("old name".to_string())),
|
||||||
preview: Some("old preview".to_string()),
|
preview: Some("old preview".to_string()),
|
||||||
is_pinned: Some(true),
|
section: Some(Some("pinned".to_string())),
|
||||||
git_info: Some(GitInfoPatch {
|
git_info: Some(GitInfoPatch {
|
||||||
sha: Some(Some("abc123".to_string())),
|
sha: Some(Some("abc123".to_string())),
|
||||||
branch: Some(Some("main".to_string())),
|
branch: Some(Some("main".to_string())),
|
||||||
@@ -990,7 +996,7 @@ mod tests {
|
|||||||
name: Some(None),
|
name: Some(None),
|
||||||
preview: None,
|
preview: None,
|
||||||
title: Some("new title".to_string()),
|
title: Some("new title".to_string()),
|
||||||
is_pinned: Some(false),
|
section: Some(None),
|
||||||
git_info: Some(GitInfoPatch {
|
git_info: Some(GitInfoPatch {
|
||||||
sha: None,
|
sha: None,
|
||||||
branch: Some(Some("feature".to_string())),
|
branch: Some(Some("feature".to_string())),
|
||||||
@@ -1002,7 +1008,7 @@ mod tests {
|
|||||||
assert_eq!(current.name, Some(None));
|
assert_eq!(current.name, Some(None));
|
||||||
assert_eq!(current.preview.as_deref(), Some("old preview"));
|
assert_eq!(current.preview.as_deref(), Some("old preview"));
|
||||||
assert_eq!(current.title.as_deref(), Some("new title"));
|
assert_eq!(current.title.as_deref(), Some("new title"));
|
||||||
assert_eq!(current.is_pinned, Some(false));
|
assert_eq!(current.section, Some(None));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
current.git_info,
|
current.git_info,
|
||||||
Some(GitInfoPatch {
|
Some(GitInfoPatch {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ impl App {
|
|||||||
model_providers: Some(vec![]),
|
model_providers: Some(vec![]),
|
||||||
source_kinds: Some(vec![ThreadSourceKind::SubAgentThreadSpawn]),
|
source_kinds: Some(vec![ThreadSourceKind::SubAgentThreadSpawn]),
|
||||||
archived: None,
|
archived: None,
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
use_state_db_only: true,
|
use_state_db_only: true,
|
||||||
search_term: None,
|
search_term: None,
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ mod tests {
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: String::new(),
|
preview: String::new(),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "openai".to_string(),
|
model_provider: "openai".to_string(),
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
|
|||||||
@@ -3459,7 +3459,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: "agent thread".to_string(),
|
preview: "agent thread".to_string(),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "agent-provider".to_string(),
|
model_provider: "agent-provider".to_string(),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
@@ -3556,7 +3556,7 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: "agent thread".to_string(),
|
preview: "agent thread".to_string(),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "agent-provider".to_string(),
|
model_provider: "agent-provider".to_string(),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
@@ -3620,7 +3620,7 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() {
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: "read thread".to_string(),
|
preview: "read thread".to_string(),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "read-provider".to_string(),
|
model_provider: "read-provider".to_string(),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ mod tests {
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: "read thread".to_string(),
|
preview: "read thread".to_string(),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "read-provider".to_string(),
|
model_provider: "read-provider".to_string(),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
|
|||||||
@@ -823,7 +823,7 @@ impl AppServerSession {
|
|||||||
request_id,
|
request_id,
|
||||||
params: ThreadMetadataUpdateParams {
|
params: ThreadMetadataUpdateParams {
|
||||||
thread_id: thread_id.to_string(),
|
thread_id: thread_id.to_string(),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
git_info: Some(ThreadMetadataGitInfoUpdateParams {
|
||||||
sha: None,
|
sha: None,
|
||||||
branch: Some(Some(branch)),
|
branch: Some(Some(branch)),
|
||||||
@@ -2679,7 +2679,7 @@ mod tests {
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: "hello".to_string(),
|
preview: "hello".to_string(),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: "openai".to_string(),
|
model_provider: "openai".to_string(),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
|
|||||||
@@ -631,7 +631,7 @@ async fn lookup_session_target_by_name_with_app_server(
|
|||||||
model_providers: None,
|
model_providers: None,
|
||||||
source_kinds: Some(vec![ThreadSourceKind::Cli, ThreadSourceKind::VsCode]),
|
source_kinds: Some(vec![ThreadSourceKind::Cli, ThreadSourceKind::VsCode]),
|
||||||
archived: Some(false),
|
archived: Some(false),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
ancestor_thread_id: None,
|
ancestor_thread_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
@@ -746,7 +746,7 @@ fn latest_session_lookup_params(
|
|||||||
},
|
},
|
||||||
source_kinds: Some(resume_source_kinds(include_non_interactive)),
|
source_kinds: Some(resume_source_kinds(include_non_interactive)),
|
||||||
archived: Some(false),
|
archived: Some(false),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
ancestor_thread_id: None,
|
ancestor_thread_id: None,
|
||||||
cwd: cwd_filter.map(|cwd| ThreadListCwdFilter::One(cwd.to_string_lossy().to_string())),
|
cwd: cwd_filter.map(|cwd| ThreadListCwdFilter::One(cwd.to_string_lossy().to_string())),
|
||||||
|
|||||||
@@ -1851,7 +1851,7 @@ fn thread_list_params(
|
|||||||
},
|
},
|
||||||
source_kinds: Some(crate::resume_source_kinds(include_non_interactive)),
|
source_kinds: Some(crate::resume_source_kinds(include_non_interactive)),
|
||||||
archived: Some(false),
|
archived: Some(false),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
ancestor_thread_id: None,
|
ancestor_thread_id: None,
|
||||||
cwd: cwd_filter.map(|cwd| ThreadListCwdFilter::One(cwd.to_string_lossy().into_owned())),
|
cwd: cwd_filter.map(|cwd| ThreadListCwdFilter::One(cwd.to_string_lossy().into_owned())),
|
||||||
@@ -5758,7 +5758,7 @@ session_picker_view = "dense"
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: String::from("remote thread"),
|
preview: String::from("remote thread"),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: String::from("openai"),
|
model_provider: String::from("openai"),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
@@ -5798,7 +5798,7 @@ session_picker_view = "dense"
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: String::from("preview"),
|
preview: String::from("preview"),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: String::from("openai"),
|
model_provider: String::from("openai"),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
@@ -5876,7 +5876,7 @@ session_picker_view = "dense"
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: String::from("preview"),
|
preview: String::from("preview"),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: String::from("openai"),
|
model_provider: String::from("openai"),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
@@ -5947,7 +5947,7 @@ session_picker_view = "dense"
|
|||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
preview: String::from("preview"),
|
preview: String::from("preview"),
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_pinned: false,
|
section: None,
|
||||||
history_mode: Default::default(),
|
history_mode: Default::default(),
|
||||||
model_provider: String::from("openai"),
|
model_provider: String::from("openai"),
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ async fn lookup_session_by_exact_name(
|
|||||||
/*include_non_interactive*/ false,
|
/*include_non_interactive*/ false,
|
||||||
)),
|
)),
|
||||||
archived: Some(archived),
|
archived: Some(archived),
|
||||||
is_pinned: None,
|
section_id: None,
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
ancestor_thread_id: None,
|
ancestor_thread_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
|
|||||||
Reference in new issue
Block a user