mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Add thread section management APIs (#36380)
## What changed - Add `threadSection/create`, `threadSection/update`, and `threadSection/delete` app-server methods, with generated protocol schemas and TypeScript bindings. - Persist custom sections in SQLite with stable UUIDv7 identities, trim and validate display names, and prevent renaming or deleting the built-in pinned section. - Delete sections transactionally and return their active and archived threads to the unsectioned list. - Serialize section mutations with section listing so concurrent requests observe ordered updates. ## Testing - Cover section persistence across restarts, ordered renames, member cleanup on deletion, invalid requests, unsupported stores, and thread-store/state-runtime behavior. GitOrigin-RevId: dc9a4be283c21dbc93ab1829eafc4825d8df6575
This commit is contained in:
1 parent
d62353a312
commit
c42ea41ee0
38 files changed
+1862
-62
No files matched your search
@@ -4590,6 +4590,32 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadSectionCreateParams": {
|
||||
"description": "Parameters for creating an independently persisted thread section.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The user-visible name of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionDeleteParams": {
|
||||
"description": "Parameters for deleting an independently persisted thread section.",
|
||||
"properties": {
|
||||
"sectionId": {
|
||||
"description": "The stable, server-generated identity of the section to delete.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sectionId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionListParams": {
|
||||
"description": "Parameters for listing independently persisted thread sections.",
|
||||
"properties": {
|
||||
@@ -4640,6 +4666,24 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionUpdateParams": {
|
||||
"description": "Parameters for updating an independently persisted thread section.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The updated user-visible name of the section.",
|
||||
"type": "string"
|
||||
},
|
||||
"sectionId": {
|
||||
"description": "The stable, server-generated identity of the section to update.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"sectionId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSetNameParams": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -5751,6 +5795,78 @@
|
||||
"title": "ThreadSection/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"threadSection/create"
|
||||
],
|
||||
"title": "ThreadSection/createRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadSectionCreateParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "ThreadSection/createRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"threadSection/update"
|
||||
],
|
||||
"title": "ThreadSection/updateRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadSectionUpdateParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "ThreadSection/updateRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"threadSection/delete"
|
||||
],
|
||||
"title": "ThreadSection/deleteRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadSectionDeleteParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "ThreadSection/deleteRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
+156
@@ -684,6 +684,78 @@
|
||||
"title": "ThreadSection/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"threadSection/create"
|
||||
],
|
||||
"title": "ThreadSection/createRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadSectionCreateParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "ThreadSection/createRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"threadSection/update"
|
||||
],
|
||||
"title": "ThreadSection/updateRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadSectionUpdateParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "ThreadSection/updateRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"threadSection/delete"
|
||||
],
|
||||
"title": "ThreadSection/deleteRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadSectionDeleteParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "ThreadSection/deleteRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -20593,6 +20665,56 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionCreateParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Parameters for creating an independently persisted thread section.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The user-visible name of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"title": "ThreadSectionCreateParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionCreateResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "The independently persisted section created by the server.",
|
||||
"properties": {
|
||||
"section": {
|
||||
"$ref": "#/definitions/v2/ThreadSection"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"section"
|
||||
],
|
||||
"title": "ThreadSectionCreateResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionDeleteParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Parameters for deleting an independently persisted thread section.",
|
||||
"properties": {
|
||||
"sectionId": {
|
||||
"description": "The stable, server-generated identity of the section to delete.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sectionId"
|
||||
],
|
||||
"title": "ThreadSectionDeleteParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionDeleteResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Successful deletion does not return additional section data.",
|
||||
"title": "ThreadSectionDeleteResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Parameters for listing independently persisted thread sections.",
|
||||
@@ -20676,6 +20798,40 @@
|
||||
"title": "ThreadSectionMoveResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionUpdateParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Parameters for updating an independently persisted thread section.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The updated user-visible name of the section.",
|
||||
"type": "string"
|
||||
},
|
||||
"sectionId": {
|
||||
"description": "The stable, server-generated identity of the section to update.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"sectionId"
|
||||
],
|
||||
"title": "ThreadSectionUpdateParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionUpdateResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "The independently persisted section after its name is updated.",
|
||||
"properties": {
|
||||
"section": {
|
||||
"$ref": "#/definitions/v2/ThreadSection"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"section"
|
||||
],
|
||||
"title": "ThreadSectionUpdateResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSetNameParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
+156
@@ -1834,6 +1834,78 @@
|
||||
"title": "ThreadSection/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"threadSection/create"
|
||||
],
|
||||
"title": "ThreadSection/createRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadSectionCreateParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "ThreadSection/createRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"threadSection/update"
|
||||
],
|
||||
"title": "ThreadSection/updateRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadSectionUpdateParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "ThreadSection/updateRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"threadSection/delete"
|
||||
],
|
||||
"title": "ThreadSection/deleteRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadSectionDeleteParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "ThreadSection/deleteRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -18357,6 +18429,56 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionCreateParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Parameters for creating an independently persisted thread section.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The user-visible name of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"title": "ThreadSectionCreateParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionCreateResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "The independently persisted section created by the server.",
|
||||
"properties": {
|
||||
"section": {
|
||||
"$ref": "#/definitions/ThreadSection"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"section"
|
||||
],
|
||||
"title": "ThreadSectionCreateResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionDeleteParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Parameters for deleting an independently persisted thread section.",
|
||||
"properties": {
|
||||
"sectionId": {
|
||||
"description": "The stable, server-generated identity of the section to delete.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sectionId"
|
||||
],
|
||||
"title": "ThreadSectionDeleteParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionDeleteResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Successful deletion does not return additional section data.",
|
||||
"title": "ThreadSectionDeleteResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Parameters for listing independently persisted thread sections.",
|
||||
@@ -18440,6 +18562,40 @@
|
||||
"title": "ThreadSectionMoveResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionUpdateParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Parameters for updating an independently persisted thread section.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The updated user-visible name of the section.",
|
||||
"type": "string"
|
||||
},
|
||||
"sectionId": {
|
||||
"description": "The stable, server-generated identity of the section to update.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"sectionId"
|
||||
],
|
||||
"title": "ThreadSectionUpdateParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSectionUpdateResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "The independently persisted section after its name is updated.",
|
||||
"properties": {
|
||||
"section": {
|
||||
"$ref": "#/definitions/ThreadSection"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"section"
|
||||
],
|
||||
"title": "ThreadSectionUpdateResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSetNameParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Parameters for creating an independently persisted thread section.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The user-visible name of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"title": "ThreadSectionCreateParams",
|
||||
"type": "object"
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"$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": "The independently persisted section created by the server.",
|
||||
"properties": {
|
||||
"section": {
|
||||
"$ref": "#/definitions/ThreadSection"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"section"
|
||||
],
|
||||
"title": "ThreadSectionCreateResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Parameters for deleting an independently persisted thread section.",
|
||||
"properties": {
|
||||
"sectionId": {
|
||||
"description": "The stable, server-generated identity of the section to delete.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sectionId"
|
||||
],
|
||||
"title": "ThreadSectionDeleteParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Successful deletion does not return additional section data.",
|
||||
"title": "ThreadSectionDeleteResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Parameters for updating an independently persisted thread section.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The updated user-visible name of the section.",
|
||||
"type": "string"
|
||||
},
|
||||
"sectionId": {
|
||||
"description": "The stable, server-generated identity of the section to update.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"sectionId"
|
||||
],
|
||||
"title": "ThreadSectionUpdateParams",
|
||||
"type": "object"
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"$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": "The independently persisted section after its name is updated.",
|
||||
"properties": {
|
||||
"section": {
|
||||
"$ref": "#/definitions/ThreadSection"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"section"
|
||||
],
|
||||
"title": "ThreadSectionUpdateResponse",
|
||||
"type": "object"
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -78,8 +78,11 @@ import type { ThreadMetadataUpdateParams } from "./v2/ThreadMetadataUpdateParams
|
||||
import type { ThreadReadParams } from "./v2/ThreadReadParams";
|
||||
import type { ThreadResumeParams } from "./v2/ThreadResumeParams";
|
||||
import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams";
|
||||
import type { ThreadSectionCreateParams } from "./v2/ThreadSectionCreateParams";
|
||||
import type { ThreadSectionDeleteParams } from "./v2/ThreadSectionDeleteParams";
|
||||
import type { ThreadSectionListParams } from "./v2/ThreadSectionListParams";
|
||||
import type { ThreadSectionMoveParams } from "./v2/ThreadSectionMoveParams";
|
||||
import type { ThreadSectionUpdateParams } from "./v2/ThreadSectionUpdateParams";
|
||||
import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams";
|
||||
import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams";
|
||||
import type { ThreadStartParams } from "./v2/ThreadStartParams";
|
||||
@@ -93,4 +96,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta
|
||||
/**
|
||||
* 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/section/move", id: RequestId, params: ThreadSectionMoveParams, } | { "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: ReLine 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/section/move", id: RequestId, params: ThreadSectionMoveParams, } | { "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": "threadSection/create", id: RequestId, params: ThreadSectionCreateParams, } | { "method": "threadSection/update", id: RequestId, params: ThreadSectionUpdateParams, } | { "method": "threadSection/delete", id: RequestId, params: ThreadSectionDeleteParams, } | { "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: ModelProviderCapabiLine truncated
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// 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 creating an independently persisted thread section.
|
||||
*/
|
||||
export type ThreadSectionCreateParams = {
|
||||
/**
|
||||
* The user-visible name of the section.
|
||||
*/
|
||||
name: string, };
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* The independently persisted section created by the server.
|
||||
*/
|
||||
export type ThreadSectionCreateResponse = { section: ThreadSection, };
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// 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 deleting an independently persisted thread section.
|
||||
*/
|
||||
export type ThreadSectionDeleteParams = {
|
||||
/**
|
||||
* The stable, server-generated identity of the section to delete.
|
||||
*/
|
||||
sectionId: string, };
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* Successful deletion does not return additional section data.
|
||||
*/
|
||||
export type ThreadSectionDeleteResponse = Record<string, never>;
|
||||
+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 updating an independently persisted thread section.
|
||||
*/
|
||||
export type ThreadSectionUpdateParams = {
|
||||
/**
|
||||
* The stable, server-generated identity of the section to update.
|
||||
*/
|
||||
sectionId: string,
|
||||
/**
|
||||
* The updated user-visible name of the section.
|
||||
*/
|
||||
name: string, };
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* The independently persisted section after its name is updated.
|
||||
*/
|
||||
export type ThreadSectionUpdateResponse = { section: ThreadSection, };
|
||||
@@ -474,10 +474,16 @@ export type { ThreadRollbackResponse } from "./ThreadRollbackResponse";
|
||||
export type { ThreadSearchResult } from "./ThreadSearchResult";
|
||||
export type { ThreadSearchSortKey } from "./ThreadSearchSortKey";
|
||||
export type { ThreadSection } from "./ThreadSection";
|
||||
export type { ThreadSectionCreateParams } from "./ThreadSectionCreateParams";
|
||||
export type { ThreadSectionCreateResponse } from "./ThreadSectionCreateResponse";
|
||||
export type { ThreadSectionDeleteParams } from "./ThreadSectionDeleteParams";
|
||||
export type { ThreadSectionDeleteResponse } from "./ThreadSectionDeleteResponse";
|
||||
export type { ThreadSectionListParams } from "./ThreadSectionListParams";
|
||||
export type { ThreadSectionListResponse } from "./ThreadSectionListResponse";
|
||||
export type { ThreadSectionMoveParams } from "./ThreadSectionMoveParams";
|
||||
export type { ThreadSectionMoveResponse } from "./ThreadSectionMoveResponse";
|
||||
export type { ThreadSectionUpdateParams } from "./ThreadSectionUpdateParams";
|
||||
export type { ThreadSectionUpdateResponse } from "./ThreadSectionUpdateResponse";
|
||||
export type { ThreadSetNameParams } from "./ThreadSetNameParams";
|
||||
export type { ThreadSetNameResponse } from "./ThreadSetNameResponse";
|
||||
export type { ThreadSettings } from "./ThreadSettings";
|
||||
|
||||
@@ -633,9 +633,24 @@ client_request_definitions! {
|
||||
},
|
||||
ThreadSectionList => "threadSection/list" {
|
||||
params: v2::ThreadSectionListParams,
|
||||
serialization: None,
|
||||
serialization: global_shared_read("thread-sections"),
|
||||
response: v2::ThreadSectionListResponse,
|
||||
},
|
||||
ThreadSectionCreate => "threadSection/create" {
|
||||
params: v2::ThreadSectionCreateParams,
|
||||
serialization: global("thread-sections"),
|
||||
response: v2::ThreadSectionCreateResponse,
|
||||
},
|
||||
ThreadSectionUpdate => "threadSection/update" {
|
||||
params: v2::ThreadSectionUpdateParams,
|
||||
serialization: global("thread-sections"),
|
||||
response: v2::ThreadSectionUpdateResponse,
|
||||
},
|
||||
ThreadSectionDelete => "threadSection/delete" {
|
||||
params: v2::ThreadSectionDeleteParams,
|
||||
serialization: global("thread-sections"),
|
||||
response: v2::ThreadSectionDeleteResponse,
|
||||
},
|
||||
#[experimental("thread/search")]
|
||||
ThreadSearch => "thread/search" {
|
||||
params: v2::ThreadSearchParams,
|
||||
|
||||
@@ -1139,6 +1139,57 @@ pub struct ThreadSectionListResponse {
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
/// Parameters for creating an independently persisted thread section.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadSectionCreateParams {
|
||||
/// The user-visible name of the section.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// The independently persisted section created by the server.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadSectionCreateResponse {
|
||||
pub section: ThreadSection,
|
||||
}
|
||||
|
||||
/// Parameters for updating an independently persisted thread section.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadSectionUpdateParams {
|
||||
/// The stable, server-generated identity of the section to update.
|
||||
pub section_id: String,
|
||||
/// The updated user-visible name of the section.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// The independently persisted section after its name is updated.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadSectionUpdateResponse {
|
||||
pub section: ThreadSection,
|
||||
}
|
||||
|
||||
/// Parameters for deleting an independently persisted thread section.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadSectionDeleteParams {
|
||||
/// The stable, server-generated identity of the section to delete.
|
||||
pub section_id: String,
|
||||
}
|
||||
|
||||
/// Successful deletion does not return additional section data.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadSectionDeleteResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -145,6 +145,9 @@ Example with notification opt-out:
|
||||
- `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`, `sectionId`, `cwd`, and `searchTerm` filters. Set `sortKey` to `"section_position"` when listing a section in its persisted manual order. 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.
|
||||
- `threadSection/create` — create a durable custom section with a server-generated UUID and a nonempty display name; returns its `section`.
|
||||
- `threadSection/update` — rename an existing custom section while retaining its stable UUID; returns the updated `section`. The built-in pinned section cannot be renamed.
|
||||
- `threadSection/delete` — delete an existing custom section and atomically return its member threads to the unsectioned list; returns `{}`. The built-in pinned section cannot be deleted.
|
||||
- `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/turns/list` — experimental; page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`.
|
||||
|
||||
@@ -1120,6 +1120,15 @@ impl MessageProcessor {
|
||||
ClientRequest::ThreadSectionList { params, .. } => {
|
||||
self.thread_processor.thread_section_list(params).await
|
||||
}
|
||||
ClientRequest::ThreadSectionCreate { params, .. } => {
|
||||
self.thread_processor.thread_section_create(params).await
|
||||
}
|
||||
ClientRequest::ThreadSectionUpdate { params, .. } => {
|
||||
self.thread_processor.thread_section_update(params).await
|
||||
}
|
||||
ClientRequest::ThreadSectionDelete { params, .. } => {
|
||||
self.thread_processor.thread_section_delete(params).await
|
||||
}
|
||||
ClientRequest::ThreadSettingsUpdate { params, .. } => {
|
||||
self.turn_processor
|
||||
.thread_settings_update(&request_id, params)
|
||||
|
||||
@@ -540,6 +540,7 @@ mod search;
|
||||
mod thread_enrichment;
|
||||
mod thread_fork_goal;
|
||||
mod thread_processor;
|
||||
mod thread_sections;
|
||||
mod token_usage_replay;
|
||||
mod turn_processor;
|
||||
mod windows_sandbox_processor;
|
||||
|
||||
@@ -5,8 +5,6 @@ use super::*;
|
||||
use crate::error_code::method_not_found;
|
||||
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_app_server_protocol::ThreadSectionMoveParams;
|
||||
use codex_app_server_protocol::ThreadSectionMoveResponse;
|
||||
use codex_extension_api::ExtensionDataInit;
|
||||
@@ -16,8 +14,8 @@ use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::protocol::ThreadHistoryMode;
|
||||
|
||||
const THREAD_LIST_DEFAULT_LIMIT: usize = 25;
|
||||
const THREAD_LIST_MAX_LIMIT: usize = 100;
|
||||
pub(super) const THREAD_LIST_DEFAULT_LIMIT: usize = 25;
|
||||
pub(super) const THREAD_LIST_MAX_LIMIT: usize = 100;
|
||||
const CODEX_TUI_CLIENT_NAME: &str = "codex-tui";
|
||||
const THREAD_ROLLBACK_DEPRECATION_SUMMARY: &str =
|
||||
"thread/rollback is deprecated and will be removed soon";
|
||||
@@ -743,39 +741,6 @@ impl ThreadRequestProcessor {
|
||||
.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(
|
||||
&self,
|
||||
params: ThreadSearchParams,
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
use super::thread_processor::THREAD_LIST_DEFAULT_LIMIT;
|
||||
use super::thread_processor::THREAD_LIST_MAX_LIMIT;
|
||||
use super::thread_processor::ThreadRequestProcessor;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_params;
|
||||
use crate::error_code::method_not_found;
|
||||
use codex_app_server_protocol::ClientResponsePayload;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::ThreadSection;
|
||||
use codex_app_server_protocol::ThreadSectionCreateParams;
|
||||
use codex_app_server_protocol::ThreadSectionCreateResponse;
|
||||
use codex_app_server_protocol::ThreadSectionDeleteParams;
|
||||
use codex_app_server_protocol::ThreadSectionDeleteResponse;
|
||||
use codex_app_server_protocol::ThreadSectionListParams;
|
||||
use codex_app_server_protocol::ThreadSectionListResponse;
|
||||
use codex_app_server_protocol::ThreadSectionUpdateParams;
|
||||
use codex_app_server_protocol::ThreadSectionUpdateResponse;
|
||||
use codex_state::PINNED_THREAD_SECTION_ID;
|
||||
use codex_thread_store::CreateThreadSectionParams as StoreCreateThreadSectionParams;
|
||||
use codex_thread_store::DeleteThreadSectionParams as StoreDeleteThreadSectionParams;
|
||||
use codex_thread_store::ListThreadSectionsParams as StoreListThreadSectionsParams;
|
||||
use codex_thread_store::RenameThreadSectionParams as StoreRenameThreadSectionParams;
|
||||
use codex_thread_store::ThreadStoreError;
|
||||
|
||||
impl ThreadRequestProcessor {
|
||||
pub(crate) async fn thread_section_list(
|
||||
&self,
|
||||
params: ThreadSectionListParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
const OPERATION: &str = "threadSection/list";
|
||||
self.ensure_thread_sections_supported(OPERATION)?;
|
||||
let limit = params
|
||||
.limit
|
||||
.map(|value| value as usize)
|
||||
.unwrap_or(THREAD_LIST_DEFAULT_LIMIT)
|
||||
.clamp(1, THREAD_LIST_MAX_LIMIT);
|
||||
let page = self
|
||||
.thread_store
|
||||
.list_thread_sections(StoreListThreadSectionsParams {
|
||||
cursor: params.cursor,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| thread_section_store_error(OPERATION, 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_section_create(
|
||||
&self,
|
||||
params: ThreadSectionCreateParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
const OPERATION: &str = "threadSection/create";
|
||||
self.ensure_thread_sections_supported(OPERATION)?;
|
||||
let name = params.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err(invalid_params("section name must not be empty"));
|
||||
}
|
||||
let section = self
|
||||
.thread_store
|
||||
.create_thread_section(StoreCreateThreadSectionParams {
|
||||
name: name.to_string(),
|
||||
})
|
||||
.await
|
||||
.map_err(|err| thread_section_store_error(OPERATION, err))?;
|
||||
|
||||
Ok(Some(
|
||||
ThreadSectionCreateResponse {
|
||||
section: ThreadSection {
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_section_update(
|
||||
&self,
|
||||
params: ThreadSectionUpdateParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
const OPERATION: &str = "threadSection/update";
|
||||
self.ensure_thread_sections_supported(OPERATION)?;
|
||||
let name = params.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err(invalid_params("section name must not be empty"));
|
||||
}
|
||||
if params.section_id.trim().is_empty() {
|
||||
return Err(invalid_params("sectionId must not be empty"));
|
||||
}
|
||||
if params.section_id == PINNED_THREAD_SECTION_ID {
|
||||
return Err(invalid_params(
|
||||
"the built-in pinned section cannot be renamed",
|
||||
));
|
||||
}
|
||||
let section = self
|
||||
.thread_store
|
||||
.rename_thread_section(StoreRenameThreadSectionParams {
|
||||
section_id: params.section_id.clone(),
|
||||
name: name.to_string(),
|
||||
})
|
||||
.await
|
||||
.map_err(|err| thread_section_store_error(OPERATION, err))?
|
||||
.ok_or_else(|| {
|
||||
invalid_params(format!("thread section not found: {}", params.section_id))
|
||||
})?;
|
||||
|
||||
Ok(Some(
|
||||
ThreadSectionUpdateResponse {
|
||||
section: ThreadSection {
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_section_delete(
|
||||
&self,
|
||||
params: ThreadSectionDeleteParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
const OPERATION: &str = "threadSection/delete";
|
||||
self.ensure_thread_sections_supported(OPERATION)?;
|
||||
if params.section_id.trim().is_empty() {
|
||||
return Err(invalid_params("sectionId must not be empty"));
|
||||
}
|
||||
if params.section_id == PINNED_THREAD_SECTION_ID {
|
||||
return Err(invalid_params(
|
||||
"the built-in pinned section cannot be deleted",
|
||||
));
|
||||
}
|
||||
let deleted = self
|
||||
.thread_store
|
||||
.delete_thread_section(StoreDeleteThreadSectionParams {
|
||||
section_id: params.section_id.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(|err| thread_section_store_error(OPERATION, err))?;
|
||||
if !deleted {
|
||||
return Err(invalid_params(format!(
|
||||
"thread section not found: {}",
|
||||
params.section_id
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Some(ThreadSectionDeleteResponse {}.into()))
|
||||
}
|
||||
|
||||
fn ensure_thread_sections_supported(
|
||||
&self,
|
||||
operation: &'static str,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
if self.thread_store.supports_thread_sections() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(unsupported_thread_section_operation(operation))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_thread_section_operation(operation: &'static str) -> JSONRPCErrorError {
|
||||
method_not_found(format!("{operation} is unavailable without sqlite state"))
|
||||
}
|
||||
|
||||
fn thread_section_store_error(
|
||||
operation: &'static str,
|
||||
error: ThreadStoreError,
|
||||
) -> JSONRPCErrorError {
|
||||
match error {
|
||||
ThreadStoreError::Unsupported { .. } => unsupported_thread_section_operation(operation),
|
||||
ThreadStoreError::InvalidRequest { message } => invalid_params(message),
|
||||
error @ (ThreadStoreError::ThreadNotFound { .. }
|
||||
| ThreadStoreError::Conflict { .. }
|
||||
| ThreadStoreError::Internal { .. }) => {
|
||||
let action = operation
|
||||
.strip_prefix("threadSection/")
|
||||
.unwrap_or(operation);
|
||||
internal_error(format!("failed to {action} thread section: {error}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ mod thread_name_websocket;
|
||||
mod thread_read;
|
||||
mod thread_resume;
|
||||
mod thread_rollback;
|
||||
mod thread_sections;
|
||||
mod thread_settings_update;
|
||||
mod thread_shell_command;
|
||||
mod thread_start;
|
||||
|
||||
@@ -37,7 +37,10 @@ use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadSectionCreateParams;
|
||||
use codex_app_server_protocol::ThreadSectionDeleteParams;
|
||||
use codex_app_server_protocol::ThreadSectionListParams;
|
||||
use codex_app_server_protocol::ThreadSectionUpdateParams;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
@@ -55,6 +58,7 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_state::PINNED_THREAD_SECTION_ID;
|
||||
use codex_thread_store::CreateThreadParams as StoreCreateThreadParams;
|
||||
use codex_thread_store::InMemoryThreadStore;
|
||||
use codex_thread_store::ThreadPersistenceMetadata;
|
||||
@@ -68,36 +72,92 @@ use uuid::Uuid;
|
||||
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<()> {
|
||||
async fn thread_section_operations_without_sqlite_return_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 {
|
||||
let section_id = Uuid::now_v7().to_string();
|
||||
|
||||
for request in [
|
||||
ClientRequest::ThreadSectionList {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: ThreadSectionListParams::default(),
|
||||
})
|
||||
.await?
|
||||
.expect_err("section discovery requires sqlite state");
|
||||
},
|
||||
ClientRequest::ThreadSectionCreate {
|
||||
request_id: RequestId::Integer(2),
|
||||
params: ThreadSectionCreateParams {
|
||||
name: "Work".to_string(),
|
||||
},
|
||||
},
|
||||
ClientRequest::ThreadSectionUpdate {
|
||||
request_id: RequestId::Integer(3),
|
||||
params: ThreadSectionUpdateParams {
|
||||
section_id: section_id.clone(),
|
||||
name: "Projects".to_string(),
|
||||
},
|
||||
},
|
||||
ClientRequest::ThreadSectionDelete {
|
||||
request_id: RequestId::Integer(4),
|
||||
params: ThreadSectionDeleteParams { section_id },
|
||||
},
|
||||
ClientRequest::ThreadSectionCreate {
|
||||
request_id: RequestId::Integer(5),
|
||||
params: ThreadSectionCreateParams {
|
||||
name: " ".to_string(),
|
||||
},
|
||||
},
|
||||
ClientRequest::ThreadSectionUpdate {
|
||||
request_id: RequestId::Integer(6),
|
||||
params: ThreadSectionUpdateParams {
|
||||
section_id: " ".to_string(),
|
||||
name: "Work".to_string(),
|
||||
},
|
||||
},
|
||||
ClientRequest::ThreadSectionUpdate {
|
||||
request_id: RequestId::Integer(7),
|
||||
params: ThreadSectionUpdateParams {
|
||||
section_id: PINNED_THREAD_SECTION_ID.to_string(),
|
||||
name: "Pinned again".to_string(),
|
||||
},
|
||||
},
|
||||
ClientRequest::ThreadSectionDelete {
|
||||
request_id: RequestId::Integer(8),
|
||||
params: ThreadSectionDeleteParams {
|
||||
section_id: " ".to_string(),
|
||||
},
|
||||
},
|
||||
ClientRequest::ThreadSectionDelete {
|
||||
request_id: RequestId::Integer(9),
|
||||
params: ThreadSectionDeleteParams {
|
||||
section_id: PINNED_THREAD_SECTION_ID.to_string(),
|
||||
},
|
||||
},
|
||||
ClientRequest::ThreadSectionUpdate {
|
||||
request_id: RequestId::Integer(10),
|
||||
params: ThreadSectionUpdateParams {
|
||||
section_id: PINNED_THREAD_SECTION_ID.to_string(),
|
||||
name: " ".to_string(),
|
||||
},
|
||||
},
|
||||
] {
|
||||
let method = request.method_name();
|
||||
let error = client
|
||||
.request(request)
|
||||
.await?
|
||||
.expect_err("section management requires sqlite state");
|
||||
|
||||
assert_eq!(error.code, -32601);
|
||||
assert_eq!(
|
||||
error.message,
|
||||
"threadSection/list is unavailable without sqlite state"
|
||||
);
|
||||
assert_eq!(error.code, -32601);
|
||||
assert_eq!(
|
||||
error.message,
|
||||
format!("{method} 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"
|
||||
);
|
||||
assert_no_local_persistence_artifacts(codex_home.path())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -466,14 +526,13 @@ fn assert_no_local_persistence_artifacts(codex_home: &Path) -> Result<()> {
|
||||
"non-local thread persistence should not create sqlite artifacts: {sqlite_artifacts:?}"
|
||||
);
|
||||
let mut entries = codex_home_entries(codex_home)?;
|
||||
// Bazel test runs may initialize shell snapshot storage under codex_home.
|
||||
// That is not thread persistence; keep the assertion focused on rollout,
|
||||
// session, sqlite, and other unexpected thread-store artifacts.
|
||||
// Host startup may leave sandbox migration markers, and Bazel test runs may
|
||||
// initialize shell snapshot storage. Neither is thread persistence.
|
||||
entries.remove(".sandbox_migration");
|
||||
entries.remove("shell_snapshots");
|
||||
assert_eq!(
|
||||
entries,
|
||||
BTreeSet::from([
|
||||
".sandbox_migration".to_string(),
|
||||
"config.toml".to_string(),
|
||||
"installation_id".to_string(),
|
||||
"skills".to_string(),
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::MockResponsesConfig;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::create_fake_rollout;
|
||||
use app_test_support::create_mock_responses_server_repeating_assistant;
|
||||
use codex_app_server::INVALID_PARAMS_ERROR_CODE;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
use codex_app_server_protocol::ThreadArchiveResponse;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::ThreadSection;
|
||||
use codex_app_server_protocol::ThreadSectionCreateParams;
|
||||
use codex_app_server_protocol::ThreadSectionCreateResponse;
|
||||
use codex_app_server_protocol::ThreadSectionDeleteParams;
|
||||
use codex_app_server_protocol::ThreadSectionDeleteResponse;
|
||||
use codex_app_server_protocol::ThreadSectionListParams;
|
||||
use codex_app_server_protocol::ThreadSectionListResponse;
|
||||
use codex_app_server_protocol::ThreadSectionMoveParams;
|
||||
use codex_app_server_protocol::ThreadSectionMoveResponse;
|
||||
use codex_app_server_protocol::ThreadSectionUpdateParams;
|
||||
use codex_app_server_protocol::ThreadSectionUpdateResponse;
|
||||
use codex_app_server_protocol::ThreadUnarchiveParams;
|
||||
use codex_app_server_protocol::ThreadUnarchiveResponse;
|
||||
use codex_features::Feature;
|
||||
use codex_state::PINNED_THREAD_SECTION_ID;
|
||||
use codex_state::PINNED_THREAD_SECTION_NAME;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
async fn section_request_error(
|
||||
server: &mut TestAppServer,
|
||||
method: &str,
|
||||
params: Value,
|
||||
) -> Result<JSONRPCError> {
|
||||
let request_id = server.send_raw_request(method, Some(params)).await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
server.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn custom_sections_remain_discoverable_across_ordered_updates_and_restart() -> Result<()> {
|
||||
let responses = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
MockResponsesConfig::new(&responses.uri())
|
||||
.enable_feature(Feature::Sqlite)
|
||||
.write(codex_home.path())?;
|
||||
let mut server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build_initialized()
|
||||
.await?;
|
||||
|
||||
let created: ThreadSectionCreateResponse = server
|
||||
.request(|request_id| ClientRequest::ThreadSectionCreate {
|
||||
request_id,
|
||||
params: ThreadSectionCreateParams {
|
||||
name: " Work ".to_string(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(created.section.name, "Work");
|
||||
assert_eq!(Uuid::parse_str(&created.section.id)?.get_version_num(), 7);
|
||||
|
||||
let retained: ThreadSectionCreateResponse = server
|
||||
.request(|request_id| ClientRequest::ThreadSectionCreate {
|
||||
request_id,
|
||||
params: ThreadSectionCreateParams {
|
||||
name: "Personal".to_string(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
let discovered: ThreadSectionListResponse = server
|
||||
.request(|request_id| ClientRequest::ThreadSectionList {
|
||||
request_id,
|
||||
params: ThreadSectionListParams {
|
||||
cursor: None,
|
||||
limit: Some(20),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(discovered.data.len(), 3);
|
||||
assert!(discovered.data.contains(&created.section));
|
||||
assert!(discovered.data.contains(&retained.section));
|
||||
|
||||
let first_rename = server
|
||||
.send_raw_request(
|
||||
"threadSection/update",
|
||||
Some(json!({ "sectionId": created.section.id, "name": "Queued work" })),
|
||||
)
|
||||
.await?;
|
||||
let second_rename = server
|
||||
.send_raw_request(
|
||||
"threadSection/update",
|
||||
Some(json!({ "sectionId": created.section.id, "name": " Projects " })),
|
||||
)
|
||||
.await?;
|
||||
let listed_after_renames = server
|
||||
.send_raw_request("threadSection/list", Some(json!({ "limit": 20 })))
|
||||
.await?;
|
||||
let _: ThreadSectionUpdateResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, server.read_response(first_rename)).await??;
|
||||
let renamed: ThreadSectionUpdateResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, server.read_response(second_rename)).await??;
|
||||
let observed: ThreadSectionListResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
server.read_response(listed_after_renames),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
renamed.section,
|
||||
ThreadSection {
|
||||
id: created.section.id,
|
||||
name: "Projects".to_string(),
|
||||
}
|
||||
);
|
||||
assert!(observed.data.contains(&renamed.section));
|
||||
|
||||
drop(server);
|
||||
let mut restarted = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let persisted: ThreadSectionListResponse = restarted
|
||||
.request(|request_id| ClientRequest::ThreadSectionList {
|
||||
request_id,
|
||||
params: ThreadSectionListParams {
|
||||
cursor: None,
|
||||
limit: Some(20),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
persisted.data,
|
||||
vec![
|
||||
ThreadSection {
|
||||
id: PINNED_THREAD_SECTION_ID.to_string(),
|
||||
name: PINNED_THREAD_SECTION_NAME.to_string(),
|
||||
},
|
||||
renamed.section,
|
||||
retained.section,
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_custom_sections_unassigns_active_and_archived_members() -> Result<()> {
|
||||
let responses = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
MockResponsesConfig::new(&responses.uri())
|
||||
.enable_feature(Feature::Sqlite)
|
||||
.write(codex_home.path())?;
|
||||
let first_thread = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-06T08-00-00",
|
||||
"2025-01-06T08:00:00Z",
|
||||
"First thread",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
let archived_thread = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-06T09-00-00",
|
||||
"2025-01-06T09:00:00Z",
|
||||
"Archived thread",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
let mut server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let request_id = server
|
||||
.send_raw_request("thread/list", Some(json!({ "limit": 20 })))
|
||||
.await?;
|
||||
let _: ThreadListResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, server.read_response(request_id)).await??;
|
||||
|
||||
let created: ThreadSectionCreateResponse = server
|
||||
.request(|request_id| ClientRequest::ThreadSectionCreate {
|
||||
request_id,
|
||||
params: ThreadSectionCreateParams {
|
||||
name: "Work".to_string(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
for thread_id in [&first_thread, &archived_thread] {
|
||||
let _: ThreadSectionMoveResponse = server
|
||||
.request(|request_id| ClientRequest::ThreadSectionMove {
|
||||
request_id,
|
||||
params: ThreadSectionMoveParams {
|
||||
thread_id: thread_id.clone(),
|
||||
section_id: Some(created.section.id.clone()),
|
||||
before_thread_id: None,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
let _: ThreadArchiveResponse = server
|
||||
.request(|request_id| ClientRequest::ThreadArchive {
|
||||
request_id,
|
||||
params: ThreadArchiveParams {
|
||||
thread_id: archived_thread.clone(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
let renamed: ThreadSectionUpdateResponse = server
|
||||
.request(|request_id| ClientRequest::ThreadSectionUpdate {
|
||||
request_id,
|
||||
params: ThreadSectionUpdateParams {
|
||||
section_id: created.section.id.clone(),
|
||||
name: "Projects".to_string(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
let renamed_member: ThreadReadResponse = server
|
||||
.request(|request_id| ClientRequest::ThreadRead {
|
||||
request_id,
|
||||
params: ThreadReadParams {
|
||||
thread_id: first_thread.clone(),
|
||||
include_turns: false,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(renamed_member.thread.section, Some(renamed.section));
|
||||
|
||||
let _: ThreadSectionDeleteResponse = server
|
||||
.request(|request_id| ClientRequest::ThreadSectionDelete {
|
||||
request_id,
|
||||
params: ThreadSectionDeleteParams {
|
||||
section_id: created.section.id,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
let unsectioned: ThreadReadResponse = server
|
||||
.request(|request_id| ClientRequest::ThreadRead {
|
||||
request_id,
|
||||
params: ThreadReadParams {
|
||||
thread_id: first_thread,
|
||||
include_turns: false,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
(
|
||||
unsectioned.thread.section,
|
||||
unsectioned.thread.section_entered_at
|
||||
),
|
||||
(None, None)
|
||||
);
|
||||
let restored: ThreadUnarchiveResponse = server
|
||||
.request(|request_id| ClientRequest::ThreadUnarchive {
|
||||
request_id,
|
||||
params: ThreadUnarchiveParams {
|
||||
thread_id: archived_thread.clone(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
(restored.thread.section, restored.thread.section_entered_at),
|
||||
(None, None)
|
||||
);
|
||||
|
||||
drop(server);
|
||||
let mut restarted = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let restored_after_restart: ThreadReadResponse = restarted
|
||||
.request(|request_id| ClientRequest::ThreadRead {
|
||||
request_id,
|
||||
params: ThreadReadParams {
|
||||
thread_id: archived_thread,
|
||||
include_turns: false,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
(
|
||||
restored_after_restart.thread.section,
|
||||
restored_after_restart.thread.section_entered_at
|
||||
),
|
||||
(None, None)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn custom_section_management_rejects_empty_names_missing_ids_and_pinned_mutations()
|
||||
-> Result<()> {
|
||||
let responses = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
MockResponsesConfig::new(&responses.uri())
|
||||
.enable_feature(Feature::Sqlite)
|
||||
.write(codex_home.path())?;
|
||||
let mut server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let missing_id = Uuid::now_v7().to_string();
|
||||
|
||||
for (method, params, message) in [
|
||||
(
|
||||
"threadSection/create",
|
||||
json!({ "name": " " }),
|
||||
"section name must not be empty",
|
||||
),
|
||||
(
|
||||
"threadSection/update",
|
||||
json!({ "sectionId": " ", "name": "Work" }),
|
||||
"sectionId must not be empty",
|
||||
),
|
||||
(
|
||||
"threadSection/update",
|
||||
json!({ "sectionId": PINNED_THREAD_SECTION_ID, "name": "Pinned again" }),
|
||||
"the built-in pinned section cannot be renamed",
|
||||
),
|
||||
(
|
||||
"threadSection/delete",
|
||||
json!({ "sectionId": PINNED_THREAD_SECTION_ID }),
|
||||
"the built-in pinned section cannot be deleted",
|
||||
),
|
||||
] {
|
||||
let error = section_request_error(&mut server, method, params).await?;
|
||||
assert_eq!(error.error.code, INVALID_PARAMS_ERROR_CODE);
|
||||
assert_eq!(error.error.message, message);
|
||||
}
|
||||
|
||||
for (method, params) in [
|
||||
(
|
||||
"threadSection/update",
|
||||
json!({ "sectionId": missing_id, "name": "Work" }),
|
||||
),
|
||||
("threadSection/delete", json!({ "sectionId": missing_id })),
|
||||
] {
|
||||
let error = section_request_error(&mut server, method, params).await?;
|
||||
assert_eq!(error.error.code, INVALID_PARAMS_ERROR_CODE);
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
format!("thread section not found: {missing_id}")
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -48,6 +48,7 @@ mod remote_control;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_support;
|
||||
mod thread_section_order;
|
||||
mod thread_sections;
|
||||
mod threads;
|
||||
|
||||
pub use external_agent_config_imports::ExternalAgentConfigImportDetailsRecord;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
use super::StateRuntime;
|
||||
use crate::PINNED_THREAD_SECTION_ID;
|
||||
use crate::ThreadSection;
|
||||
use uuid::Uuid;
|
||||
|
||||
impl StateRuntime {
|
||||
/// Create a custom thread section with a stable, server-assigned UUIDv7.
|
||||
pub async fn create_thread_section(&self, name: &str) -> anyhow::Result<ThreadSection> {
|
||||
let section = ThreadSection {
|
||||
id: Uuid::now_v7().to_string(),
|
||||
name: name.to_string(),
|
||||
};
|
||||
|
||||
sqlx::query("INSERT INTO thread_sections (id, name) VALUES (?, ?)")
|
||||
.bind(§ion.id)
|
||||
.bind(§ion.name)
|
||||
.execute(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(section)
|
||||
}
|
||||
|
||||
/// Rename a custom thread section without changing its stable identity.
|
||||
pub async fn rename_thread_section(
|
||||
&self,
|
||||
id: &str,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<ThreadSection>> {
|
||||
if id == PINNED_THREAD_SECTION_ID {
|
||||
anyhow::bail!("built-in pinned thread section cannot be renamed");
|
||||
}
|
||||
|
||||
let section = sqlx::query_as::<_, (String, String)>(
|
||||
"UPDATE thread_sections SET name = ? WHERE id = ? RETURNING id, name",
|
||||
)
|
||||
.bind(name)
|
||||
.bind(id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(section.map(|(id, name)| ThreadSection { id, name }))
|
||||
}
|
||||
|
||||
/// Delete a custom section and return its threads to the unsectioned list.
|
||||
pub async fn delete_thread_section(&self, id: &str) -> anyhow::Result<bool> {
|
||||
if id == PINNED_THREAD_SECTION_ID {
|
||||
anyhow::bail!("built-in pinned thread section cannot be deleted");
|
||||
}
|
||||
|
||||
let mut tx = self.pool.begin_with("BEGIN IMMEDIATE").await?;
|
||||
sqlx::query(
|
||||
"UPDATE threads SET section_position = NULL, section_entered_at_ms = NULL WHERE thread_section_id = ?",
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let deleted = sqlx::query("DELETE FROM thread_sections WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await?
|
||||
.rows_affected()
|
||||
> 0;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(deleted)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "thread_sections_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,115 @@
|
||||
use super::StateRuntime;
|
||||
use crate::runtime::test_support::test_thread_metadata;
|
||||
use crate::runtime::test_support::unique_temp_dir;
|
||||
use anyhow::Result;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_utils_absolute_path::test_support::PathExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_custom_section_preserves_threads_and_clears_section_ordering() -> Result<()> {
|
||||
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?;
|
||||
let deleted_section = runtime.create_thread_section("Research").await?;
|
||||
let retained_section = runtime.create_thread_section("Planning").await?;
|
||||
let first = ThreadId::new();
|
||||
let second = ThreadId::new();
|
||||
let retained = ThreadId::new();
|
||||
|
||||
for (thread_id, section_id) in [
|
||||
(first, &deleted_section.id),
|
||||
(second, &deleted_section.id),
|
||||
(retained, &retained_section.id),
|
||||
] {
|
||||
runtime
|
||||
.upsert_thread(&test_thread_metadata(
|
||||
&codex_home,
|
||||
thread_id,
|
||||
codex_home.clone(),
|
||||
))
|
||||
.await?;
|
||||
runtime
|
||||
.move_thread_to_section(thread_id, Some(section_id), /*before_thread_id*/ None)
|
||||
.await?;
|
||||
}
|
||||
|
||||
assert!(runtime.delete_thread_section(&deleted_section.id).await?);
|
||||
assert_eq!(runtime.get_thread_section(&deleted_section.id).await?, None);
|
||||
|
||||
for thread_id in [first, second] {
|
||||
let thread = runtime
|
||||
.get_thread(thread_id)
|
||||
.await?
|
||||
.expect("section deletion must preserve its member threads");
|
||||
assert_eq!(
|
||||
(
|
||||
thread.section,
|
||||
thread.section_position,
|
||||
thread.section_entered_at
|
||||
),
|
||||
(None, None, None)
|
||||
);
|
||||
}
|
||||
|
||||
let retained_thread = runtime
|
||||
.get_thread(retained)
|
||||
.await?
|
||||
.expect("unrelated section member should remain");
|
||||
assert_eq!(retained_thread.section, Some(retained_section));
|
||||
assert!(retained_thread.section_position.is_some());
|
||||
assert!(retained_thread.section_entered_at.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_section_deletion_and_membership_moves_preserve_thread_invariants() -> Result<()>
|
||||
{
|
||||
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?;
|
||||
let section = runtime.create_thread_section("Research").await?;
|
||||
let thread_id = ThreadId::new();
|
||||
runtime
|
||||
.upsert_thread(&test_thread_metadata(
|
||||
&codex_home,
|
||||
thread_id,
|
||||
codex_home.clone(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
let (deleted, moved) = tokio::join!(
|
||||
runtime.delete_thread_section(§ion.id),
|
||||
runtime.move_thread_to_section(
|
||||
thread_id,
|
||||
Some(§ion.id),
|
||||
/*before_thread_id*/ None
|
||||
),
|
||||
);
|
||||
|
||||
assert!(deleted?);
|
||||
if let Err(error) = moved {
|
||||
assert!(error.to_string().contains("does not exist"));
|
||||
}
|
||||
let thread = runtime
|
||||
.get_thread(thread_id)
|
||||
.await?
|
||||
.expect("concurrent section deletion must preserve the thread");
|
||||
assert_eq!(
|
||||
(
|
||||
thread.section,
|
||||
thread.section_position,
|
||||
thread.section_entered_at
|
||||
),
|
||||
(None, None, None)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -10,6 +10,7 @@ mod live_thread;
|
||||
mod local;
|
||||
mod store;
|
||||
mod thread_metadata_sync;
|
||||
mod thread_sections;
|
||||
mod types;
|
||||
|
||||
pub use error::ThreadStoreError;
|
||||
@@ -22,6 +23,12 @@ pub use local::LocalThreadStore;
|
||||
pub use local::LocalThreadStoreConfig;
|
||||
pub use store::ThreadStore;
|
||||
pub use store::ThreadStoreFuture;
|
||||
pub use thread_sections::CreateThreadSectionParams;
|
||||
pub use thread_sections::DeleteThreadSectionParams;
|
||||
pub use thread_sections::ListThreadSectionsParams;
|
||||
pub use thread_sections::RenameThreadSectionParams;
|
||||
pub use thread_sections::StoredThreadSection;
|
||||
pub use thread_sections::StoredThreadSectionsPage;
|
||||
pub use types::AppendThreadItemsParams;
|
||||
pub use types::ArchiveThreadParams;
|
||||
pub use types::ArchiveThreadsParams;
|
||||
|
||||
@@ -14,6 +14,7 @@ mod rollout_lineage;
|
||||
mod search_threads;
|
||||
mod thread_history;
|
||||
mod thread_history_materialization;
|
||||
mod thread_sections;
|
||||
mod unarchive_thread;
|
||||
mod update_thread_metadata;
|
||||
mod writer_lock;
|
||||
@@ -41,10 +42,13 @@ use crate::AppendThreadItemsParams;
|
||||
use crate::ArchiveThreadParams;
|
||||
use crate::ArchiveThreadsParams;
|
||||
use crate::CreateThreadParams;
|
||||
use crate::CreateThreadSectionParams;
|
||||
use crate::DeleteThreadParams;
|
||||
use crate::DeleteThreadSectionParams;
|
||||
use crate::DeleteThreadsParams;
|
||||
use crate::ItemPage;
|
||||
use crate::ListItemsParams;
|
||||
use crate::ListThreadSectionsParams;
|
||||
use crate::ListThreadsParams;
|
||||
use crate::ListTurnsParams;
|
||||
use crate::LoadThreadHistoryParams;
|
||||
@@ -53,12 +57,15 @@ use crate::PrepareForkParams;
|
||||
use crate::PreparedFork;
|
||||
use crate::ReadThreadByRolloutPathParams;
|
||||
use crate::ReadThreadParams;
|
||||
use crate::RenameThreadSectionParams;
|
||||
use crate::ResumeThreadParams;
|
||||
use crate::SearchThreadOccurrencesParams;
|
||||
use crate::SearchThreadsParams;
|
||||
use crate::StoredModelContext;
|
||||
use crate::StoredThread;
|
||||
use crate::StoredThreadHistory;
|
||||
use crate::StoredThreadSection;
|
||||
use crate::StoredThreadSectionsPage;
|
||||
use crate::ThreadOccurrenceSearchPage;
|
||||
use crate::ThreadPage;
|
||||
use crate::ThreadSearchPage;
|
||||
@@ -458,6 +465,38 @@ impl ThreadStore for LocalThreadStore {
|
||||
Box::pin(async move { list_threads::list_threads(self, params).await })
|
||||
}
|
||||
|
||||
fn supports_thread_sections(&self) -> bool {
|
||||
self.state_db.is_some()
|
||||
}
|
||||
|
||||
fn list_thread_sections(
|
||||
&self,
|
||||
params: ListThreadSectionsParams,
|
||||
) -> ThreadStoreFuture<'_, StoredThreadSectionsPage> {
|
||||
Box::pin(async move { thread_sections::list_thread_sections(self, params).await })
|
||||
}
|
||||
|
||||
fn create_thread_section(
|
||||
&self,
|
||||
params: CreateThreadSectionParams,
|
||||
) -> ThreadStoreFuture<'_, StoredThreadSection> {
|
||||
Box::pin(async move { thread_sections::create_thread_section(self, params).await })
|
||||
}
|
||||
|
||||
fn rename_thread_section(
|
||||
&self,
|
||||
params: RenameThreadSectionParams,
|
||||
) -> ThreadStoreFuture<'_, Option<StoredThreadSection>> {
|
||||
Box::pin(async move { thread_sections::rename_thread_section(self, params).await })
|
||||
}
|
||||
|
||||
fn delete_thread_section(
|
||||
&self,
|
||||
params: DeleteThreadSectionParams,
|
||||
) -> ThreadStoreFuture<'_, bool> {
|
||||
Box::pin(async move { thread_sections::delete_thread_section(self, params).await })
|
||||
}
|
||||
|
||||
fn supports_paginated_history_lists(&self) -> bool {
|
||||
self.state_db.is_some()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
use super::LocalThreadStore;
|
||||
use crate::CreateThreadSectionParams;
|
||||
use crate::DeleteThreadSectionParams;
|
||||
use crate::ListThreadSectionsParams;
|
||||
use crate::RenameThreadSectionParams;
|
||||
use crate::StoredThreadSection;
|
||||
use crate::StoredThreadSectionsPage;
|
||||
use crate::ThreadStoreError;
|
||||
use crate::ThreadStoreResult;
|
||||
use codex_rollout::StateDbHandle;
|
||||
|
||||
pub(super) async fn list_thread_sections(
|
||||
store: &LocalThreadStore,
|
||||
params: ListThreadSectionsParams,
|
||||
) -> ThreadStoreResult<StoredThreadSectionsPage> {
|
||||
let state_db = state_db(store, "threadSection/list")?;
|
||||
let page = state_db
|
||||
.list_thread_sections(params.cursor.as_deref(), params.limit)
|
||||
.await
|
||||
.map_err(|err| section_error("list", err))?;
|
||||
|
||||
Ok(StoredThreadSectionsPage {
|
||||
sections: page
|
||||
.sections
|
||||
.into_iter()
|
||||
.map(|section| StoredThreadSection {
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
})
|
||||
.collect(),
|
||||
next_cursor: page.next_cursor,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn create_thread_section(
|
||||
store: &LocalThreadStore,
|
||||
params: CreateThreadSectionParams,
|
||||
) -> ThreadStoreResult<StoredThreadSection> {
|
||||
let state_db = state_db(store, "threadSection/create")?;
|
||||
let section = state_db
|
||||
.create_thread_section(¶ms.name)
|
||||
.await
|
||||
.map_err(|err| section_error("create", err))?;
|
||||
|
||||
Ok(StoredThreadSection {
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn rename_thread_section(
|
||||
store: &LocalThreadStore,
|
||||
params: RenameThreadSectionParams,
|
||||
) -> ThreadStoreResult<Option<StoredThreadSection>> {
|
||||
let state_db = state_db(store, "threadSection/update")?;
|
||||
let section = state_db
|
||||
.rename_thread_section(¶ms.section_id, ¶ms.name)
|
||||
.await
|
||||
.map_err(|err| section_error("update", err))?;
|
||||
|
||||
Ok(section.map(|section| StoredThreadSection {
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn delete_thread_section(
|
||||
store: &LocalThreadStore,
|
||||
params: DeleteThreadSectionParams,
|
||||
) -> ThreadStoreResult<bool> {
|
||||
let state_db = state_db(store, "threadSection/delete")?;
|
||||
state_db
|
||||
.delete_thread_section(¶ms.section_id)
|
||||
.await
|
||||
.map_err(|err| section_error("delete", err))
|
||||
}
|
||||
|
||||
fn state_db<'store>(
|
||||
store: &'store LocalThreadStore,
|
||||
operation: &'static str,
|
||||
) -> ThreadStoreResult<&'store StateDbHandle> {
|
||||
store
|
||||
.state_db
|
||||
.as_ref()
|
||||
.ok_or(ThreadStoreError::Unsupported { operation })
|
||||
}
|
||||
|
||||
fn section_error(operation: &str, error: impl std::fmt::Display) -> ThreadStoreError {
|
||||
ThreadStoreError::Internal {
|
||||
message: format!("failed to {operation} thread section: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "thread_sections_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,76 @@
|
||||
use super::LocalThreadStore;
|
||||
use crate::CreateThreadSectionParams;
|
||||
use crate::DeleteThreadSectionParams;
|
||||
use crate::ListThreadSectionsParams;
|
||||
use crate::RenameThreadSectionParams;
|
||||
use crate::StoredThreadSection;
|
||||
use crate::ThreadStore;
|
||||
use crate::ThreadStoreError;
|
||||
use crate::local::test_support::test_config;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_thread_sections_require_sqlite_and_preserve_section_identity() {
|
||||
let home = tempfile::TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let unsupported_store = LocalThreadStore::new(config.clone(), /*state_db*/ None);
|
||||
assert!(!unsupported_store.supports_thread_sections());
|
||||
assert!(matches!(
|
||||
unsupported_store
|
||||
.create_thread_section(CreateThreadSectionParams {
|
||||
name: "Research".to_string(),
|
||||
})
|
||||
.await,
|
||||
Err(ThreadStoreError::Unsupported {
|
||||
operation: "threadSection/create"
|
||||
})
|
||||
));
|
||||
|
||||
let runtime = codex_state::StateRuntime::init(
|
||||
config.sqlite.clone(),
|
||||
config.default_model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config, Some(runtime));
|
||||
assert!(store.supports_thread_sections());
|
||||
|
||||
let section = store
|
||||
.create_thread_section(CreateThreadSectionParams {
|
||||
name: "Research".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("create section");
|
||||
let sections = store
|
||||
.list_thread_sections(ListThreadSectionsParams {
|
||||
cursor: None,
|
||||
limit: 10,
|
||||
})
|
||||
.await
|
||||
.expect("list sections");
|
||||
assert!(sections.sections.contains(§ion));
|
||||
|
||||
let renamed = store
|
||||
.rename_thread_section(RenameThreadSectionParams {
|
||||
section_id: section.id.clone(),
|
||||
name: "Planning".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("rename section");
|
||||
assert_eq!(
|
||||
renamed,
|
||||
Some(StoredThreadSection {
|
||||
id: section.id.clone(),
|
||||
name: "Planning".to_string(),
|
||||
})
|
||||
);
|
||||
|
||||
assert!(
|
||||
store
|
||||
.delete_thread_section(DeleteThreadSectionParams {
|
||||
section_id: section.id,
|
||||
})
|
||||
.await
|
||||
.expect("delete section")
|
||||
);
|
||||
}
|
||||
@@ -8,10 +8,13 @@ use crate::AppendThreadItemsParams;
|
||||
use crate::ArchiveThreadParams;
|
||||
use crate::ArchiveThreadsParams;
|
||||
use crate::CreateThreadParams;
|
||||
use crate::CreateThreadSectionParams;
|
||||
use crate::DeleteThreadParams;
|
||||
use crate::DeleteThreadSectionParams;
|
||||
use crate::DeleteThreadsParams;
|
||||
use crate::ItemPage;
|
||||
use crate::ListItemsParams;
|
||||
use crate::ListThreadSectionsParams;
|
||||
use crate::ListThreadsParams;
|
||||
use crate::ListTurnsParams;
|
||||
use crate::LoadThreadHistoryParams;
|
||||
@@ -20,12 +23,15 @@ use crate::PrepareForkParams;
|
||||
use crate::PreparedFork;
|
||||
use crate::ReadThreadByRolloutPathParams;
|
||||
use crate::ReadThreadParams;
|
||||
use crate::RenameThreadSectionParams;
|
||||
use crate::ResumeThreadParams;
|
||||
use crate::SearchThreadOccurrencesParams;
|
||||
use crate::SearchThreadsParams;
|
||||
use crate::StoredModelContext;
|
||||
use crate::StoredThread;
|
||||
use crate::StoredThreadHistory;
|
||||
use crate::StoredThreadSection;
|
||||
use crate::StoredThreadSectionsPage;
|
||||
use crate::ThreadOccurrenceSearchPage;
|
||||
use crate::ThreadPage;
|
||||
use crate::ThreadSearchPage;
|
||||
@@ -123,6 +129,59 @@ pub trait ThreadStore: Any + Send + Sync {
|
||||
/// Lists stored threads matching the supplied filters.
|
||||
fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreFuture<'_, ThreadPage>;
|
||||
|
||||
/// Whether this store can discover and manage independently persisted thread sections.
|
||||
fn supports_thread_sections(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Lists independently persisted thread sections.
|
||||
fn list_thread_sections(
|
||||
&self,
|
||||
_params: ListThreadSectionsParams,
|
||||
) -> ThreadStoreFuture<'_, StoredThreadSectionsPage> {
|
||||
Box::pin(async {
|
||||
Err(ThreadStoreError::Unsupported {
|
||||
operation: "threadSection/list",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a custom thread section with a stable, server-assigned identity.
|
||||
fn create_thread_section(
|
||||
&self,
|
||||
_params: CreateThreadSectionParams,
|
||||
) -> ThreadStoreFuture<'_, StoredThreadSection> {
|
||||
Box::pin(async {
|
||||
Err(ThreadStoreError::Unsupported {
|
||||
operation: "threadSection/create",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Renames a custom thread section, returning `None` when it does not exist.
|
||||
fn rename_thread_section(
|
||||
&self,
|
||||
_params: RenameThreadSectionParams,
|
||||
) -> ThreadStoreFuture<'_, Option<StoredThreadSection>> {
|
||||
Box::pin(async {
|
||||
Err(ThreadStoreError::Unsupported {
|
||||
operation: "threadSection/update",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Deletes a custom thread section and reports whether it existed.
|
||||
fn delete_thread_section(
|
||||
&self,
|
||||
_params: DeleteThreadSectionParams,
|
||||
) -> ThreadStoreFuture<'_, bool> {
|
||||
Box::pin(async {
|
||||
Err(ThreadStoreError::Unsupported {
|
||||
operation: "threadSection/delete",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether paginated threads can hydrate durable history through turn and item lists.
|
||||
fn supports_paginated_history_lists(&self) -> bool {
|
||||
false
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/// Parameters for listing independently persisted thread sections.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ListThreadSectionsParams {
|
||||
/// Opaque cursor returned by a previous section listing.
|
||||
pub cursor: Option<String>,
|
||||
/// Maximum number of sections to return.
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
/// Parameters for creating a thread section.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CreateThreadSectionParams {
|
||||
/// User-facing section name.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Parameters for renaming a thread section.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RenameThreadSectionParams {
|
||||
/// Stable identifier of the section to rename.
|
||||
pub section_id: String,
|
||||
/// Replacement user-facing section name.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Parameters for deleting a thread section.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DeleteThreadSectionParams {
|
||||
/// Stable identifier of the section to delete.
|
||||
pub section_id: String,
|
||||
}
|
||||
|
||||
/// An independently persisted thread section and its user-facing name.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredThreadSection {
|
||||
/// Stable, server-owned section identifier.
|
||||
pub id: String,
|
||||
/// User-facing section name.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// A cursor-paginated page of independently persisted thread sections.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredThreadSectionsPage {
|
||||
/// Sections returned for this page.
|
||||
pub sections: Vec<StoredThreadSection>,
|
||||
/// Opaque cursor to continue listing sections.
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
Reference in new issue
Block a user