diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index ae8e6fed34..2e510a6424 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -946,6 +946,28 @@ ], "type": "string" }, + "InboxListParams": { + "description": "List tracked inbox entries from `$CODEX_HOME/inbox`.", + "type": "object" + }, + "InboxUpdateParams": { + "description": "Update user-owned inbox tracking state for one record.", + "properties": { + "lastReadAt": { + "description": "RFC 3339 timestamp to store in `tracking.last_read_at`.", + "type": "string" + }, + "threadId": { + "description": "Exact inbox filename under `$CODEX_HOME/inbox`, for example `thread__20260319T170500Z__slack__C01234567__1712345678.000000.json`.", + "type": "string" + } + }, + "required": [ + "lastReadAt", + "threadId" + ], + "type": "object" + }, "InitializeCapabilities": { "description": "Client-declared capabilities negotiated during initialize.", "properties": { @@ -3818,6 +3840,54 @@ "title": "App/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "inbox/list" + ], + "title": "Inbox/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/InboxListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Inbox/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "inbox/update" + ], + "title": "Inbox/updateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/InboxUpdateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Inbox/updateRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 68bf7477e5..fd4b9cdfa8 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -715,6 +715,54 @@ "title": "App/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "inbox/list" + ], + "title": "Inbox/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/InboxListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Inbox/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "inbox/update" + ], + "title": "Inbox/updateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/InboxUpdateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Inbox/updateRequest", + "type": "object" + }, { "properties": { "id": { @@ -8131,6 +8179,58 @@ ], "type": "string" }, + "InboxListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "List tracked inbox entries from `$CODEX_HOME/inbox`.", + "title": "InboxListParams", + "type": "object" + }, + "InboxListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Tracked inbox records are returned as the on-disk JSON shape defined by the sort-inbox skill's thread record format.", + "properties": { + "data": { + "items": true, + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "InboxListResponse", + "type": "object" + }, + "InboxUpdateParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Update user-owned inbox tracking state for one record.", + "properties": { + "lastReadAt": { + "description": "RFC 3339 timestamp to store in `tracking.last_read_at`.", + "type": "string" + }, + "threadId": { + "description": "Exact inbox filename under `$CODEX_HOME/inbox`, for example `thread__20260319T170500Z__slack__C01234567__1712345678.000000.json`.", + "type": "string" + } + }, + "required": [ + "lastReadAt", + "threadId" + ], + "title": "InboxUpdateParams", + "type": "object" + }, + "InboxUpdateResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "entry": true + }, + "required": [ + "entry" + ], + "title": "InboxUpdateResponse", + "type": "object" + }, "InputModality": { "description": "Canonical user-input modality tags advertised by a model.", "oneOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 772eb6f47a..54fbaf8dce 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -1246,6 +1246,54 @@ "title": "App/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "inbox/list" + ], + "title": "Inbox/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/InboxListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Inbox/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "inbox/update" + ], + "title": "Inbox/updateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/InboxUpdateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Inbox/updateRequest", + "type": "object" + }, { "properties": { "id": { @@ -4855,6 +4903,58 @@ ], "type": "string" }, + "InboxListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "List tracked inbox entries from `$CODEX_HOME/inbox`.", + "title": "InboxListParams", + "type": "object" + }, + "InboxListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Tracked inbox records are returned as the on-disk JSON shape defined by the sort-inbox skill's thread record format.", + "properties": { + "data": { + "items": true, + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "InboxListResponse", + "type": "object" + }, + "InboxUpdateParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Update user-owned inbox tracking state for one record.", + "properties": { + "lastReadAt": { + "description": "RFC 3339 timestamp to store in `tracking.last_read_at`.", + "type": "string" + }, + "threadId": { + "description": "Exact inbox filename under `$CODEX_HOME/inbox`, for example `thread__20260319T170500Z__slack__C01234567__1712345678.000000.json`.", + "type": "string" + } + }, + "required": [ + "lastReadAt", + "threadId" + ], + "title": "InboxUpdateParams", + "type": "object" + }, + "InboxUpdateResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "entry": true + }, + "required": [ + "entry" + ], + "title": "InboxUpdateResponse", + "type": "object" + }, "InitializeCapabilities": { "description": "Client-declared capabilities negotiated during initialize.", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/InboxListParams.json b/codex-rs/app-server-protocol/schema/json/v2/InboxListParams.json new file mode 100644 index 0000000000..26f05c069b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/InboxListParams.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "List tracked inbox entries from `$CODEX_HOME/inbox`.", + "title": "InboxListParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/InboxListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/InboxListResponse.json new file mode 100644 index 0000000000..995269dca8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/InboxListResponse.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Tracked inbox records are returned as the on-disk JSON shape defined by the sort-inbox skill's thread record format.", + "properties": { + "data": { + "items": true, + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "InboxListResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/InboxUpdateParams.json b/codex-rs/app-server-protocol/schema/json/v2/InboxUpdateParams.json new file mode 100644 index 0000000000..0c7171d720 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/InboxUpdateParams.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Update user-owned inbox tracking state for one record.", + "properties": { + "lastReadAt": { + "description": "RFC 3339 timestamp to store in `tracking.last_read_at`.", + "type": "string" + }, + "threadId": { + "description": "Exact inbox filename under `$CODEX_HOME/inbox`, for example `thread__20260319T170500Z__slack__C01234567__1712345678.000000.json`.", + "type": "string" + } + }, + "required": [ + "lastReadAt", + "threadId" + ], + "title": "InboxUpdateParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/InboxUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/InboxUpdateResponse.json new file mode 100644 index 0000000000..9e600690fb --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/InboxUpdateResponse.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "entry": true + }, + "required": [ + "entry" + ], + "title": "InboxUpdateResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 5e03a26ca2..9108328918 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -28,6 +28,8 @@ import type { FsReadFileParams } from "./v2/FsReadFileParams"; import type { FsRemoveParams } from "./v2/FsRemoveParams"; import type { FsWriteFileParams } from "./v2/FsWriteFileParams"; import type { GetAccountParams } from "./v2/GetAccountParams"; +import type { InboxListParams } from "./v2/InboxListParams"; +import type { InboxUpdateParams } from "./v2/InboxUpdateParams"; import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams"; import type { LoginAccountParams } from "./v2/LoginAccountParams"; import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams"; @@ -61,4 +63,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/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "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/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": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "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": "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": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +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/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "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/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": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "inbox/list", id: RequestId, params: InboxListParams, } | { "method": "inbox/update", id: RequestId, params: InboxUpdateParams, } | { "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": "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": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/InboxListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/InboxListParams.ts new file mode 100644 index 0000000000..27d542c76d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/InboxListParams.ts @@ -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. + +/** + * List tracked inbox entries from `$CODEX_HOME/inbox`. + */ +export type InboxListParams = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/InboxListResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/InboxListResponse.ts new file mode 100644 index 0000000000..7b4b3e4128 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/InboxListResponse.ts @@ -0,0 +1,10 @@ +// 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 { JsonValue } from "../serde_json/JsonValue"; + +/** + * Tracked inbox records are returned as the on-disk JSON shape defined by the + * sort-inbox skill's thread record format. + */ +export type InboxListResponse = { data: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/InboxUpdateParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/InboxUpdateParams.ts new file mode 100644 index 0000000000..8006963f13 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/InboxUpdateParams.ts @@ -0,0 +1,17 @@ +// 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. + +/** + * Update user-owned inbox tracking state for one record. + */ +export type InboxUpdateParams = { +/** + * Exact inbox filename under `$CODEX_HOME/inbox`, for example + * `thread__20260319T170500Z__slack__C01234567__1712345678.000000.json`. + */ +threadId: string, +/** + * RFC 3339 timestamp to store in `tracking.last_read_at`. + */ +lastReadAt: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/InboxUpdateResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/InboxUpdateResponse.ts new file mode 100644 index 0000000000..9fa1209e45 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/InboxUpdateResponse.ts @@ -0,0 +1,6 @@ +// 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 { JsonValue } from "../serde_json/JsonValue"; + +export type InboxUpdateResponse = { entry: JsonValue, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index f98d7676ff..ba5a7691ef 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -131,6 +131,10 @@ export type { HookRunStatus } from "./HookRunStatus"; export type { HookRunSummary } from "./HookRunSummary"; export type { HookScope } from "./HookScope"; export type { HookStartedNotification } from "./HookStartedNotification"; +export type { InboxListParams } from "./InboxListParams"; +export type { InboxListResponse } from "./InboxListResponse"; +export type { InboxUpdateParams } from "./InboxUpdateParams"; +export type { InboxUpdateResponse } from "./InboxUpdateResponse"; export type { ItemCompletedNotification } from "./ItemCompletedNotification"; export type { ItemGuardianApprovalReviewCompletedNotification } from "./ItemGuardianApprovalReviewCompletedNotification"; export type { ItemGuardianApprovalReviewStartedNotification } from "./ItemGuardianApprovalReviewStartedNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 7e1dc78f20..2dd7ffa2b5 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -308,6 +308,14 @@ client_request_definitions! { params: v2::AppsListParams, response: v2::AppsListResponse, }, + InboxList => "inbox/list" { + params: v2::InboxListParams, + response: v2::InboxListResponse, + }, + InboxUpdate => "inbox/update" { + params: v2::InboxUpdateParams, + response: v2::InboxUpdateResponse, + }, FsReadFile => "fs/readFile" { params: v2::FsReadFileParams, response: v2::FsReadFileResponse, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 1c8903a144..1548a33548 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2061,6 +2061,40 @@ pub struct AppsListResponse { pub next_cursor: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// List tracked inbox entries from `$CODEX_HOME/inbox`. +pub struct InboxListParams {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// Tracked inbox records are returned as the on-disk JSON shape defined by the +/// sort-inbox skill's thread record format. +pub struct InboxListResponse { + pub data: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// Update user-owned inbox tracking state for one record. +pub struct InboxUpdateParams { + /// Exact inbox filename under `$CODEX_HOME/inbox`, for example + /// `thread__20260319T170500Z__slack__C01234567__1712345678.000000.json`. + pub thread_id: String, + /// RFC 3339 timestamp to store in `tracking.last_read_at`. + pub last_read_at: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct InboxUpdateResponse { + pub entry: serde_json::Value, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 7959c61aa5..cbd327c7be 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1215,6 +1215,56 @@ The server also emits `app/list/updated` notifications whenever either source (a } ``` +## Inbox + +Use `inbox/list` to load tracked inbox records from `$CODEX_HOME/inbox` (or `~/.codex/inbox` when `CODEX_HOME` is unset). The server returns the thread record JSON defined by the sort-inbox skill's format, ordered newest-first by filename, with user-owned tracking fields overlaid from `$CODEX_HOME/inbox/tracking.json`. Malformed `thread__*.json` files are skipped so one bad record does not block the rest. + +```json +{ "method": "inbox/list", "id": 51, "params": {} } +{ "id": 51, "result": { + "data": [ + { + "schema_version": 1, + "source": { + "type": "slack", + "channel_id": "C01234567", + "thread_ts": "1712345678.000000", + "thread_url": "https://..." + }, + "participants": [], + "tracking": { + "last_refreshed_at": "2026-03-19T17:05:00-07:00" + }, + "current_progress": "Short summary of where the thread stands now.", + "context": { + "slack_threads": [], + "notion_pages": [], + "google_docs": [] + }, + "timeline": [] + } + ] +} } +``` + +Use `inbox/update` to update user-owned tracking state for a single inbox record by filename. This writes to `$CODEX_HOME/inbox/tracking.json` instead of mutating the thread record file, so model-owned and user-owned writes do not overlap. + +```json +{ "method": "inbox/update", "id": 52, "params": { + "threadId": "thread__20260320T012640Z__slack__C08MGJXUCUQ__1773969612.866769.json", + "lastReadAt": "2026-03-20T09:15:00-07:00" +} } +{ "id": 52, "result": { + "entry": { + "schema_version": 1, + "tracking": { + "last_read_at": "2026-03-20T09:15:00-07:00", + "last_refreshed_at": "2026-03-19T18:54:47-07:00" + } + } +} } +``` + Invoke an app by inserting `$` in the text input. The slug is derived from the app name and lowercased with non-alphanumeric characters replaced by `-` (for example, "Demo App" becomes `$demo-app`). Add a `mention` input item (recommended) so the server uses the exact `app://` path rather than guessing by name. Plugins use the same `mention` item shape, but with `plugin://@` paths from `plugin/list`. Example: diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 13e863b7fc..4f57d5f7cd 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -64,6 +64,10 @@ use codex_app_server_protocol::GetConversationSummaryParams; use codex_app_server_protocol::GetConversationSummaryResponse; use codex_app_server_protocol::GitDiffToRemoteResponse; use codex_app_server_protocol::GitInfo as ApiGitInfo; +use codex_app_server_protocol::InboxListParams; +use codex_app_server_protocol::InboxListResponse; +use codex_app_server_protocol::InboxUpdateParams; +use codex_app_server_protocol::InboxUpdateResponse; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::ListMcpServerStatusParams; use codex_app_server_protocol::ListMcpServerStatusResponse; @@ -310,6 +314,7 @@ use uuid::Uuid; use codex_app_server_protocol::ServerRequest; mod apps_list_helpers; +mod inbox_list_helpers; mod plugin_app_helpers; use crate::filters::compute_source_filters; @@ -717,6 +722,14 @@ impl CodexMessageProcessor { self.apps_list(to_connection_request_id(request_id), params) .await; } + ClientRequest::InboxList { request_id, params } => { + self.inbox_list(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::InboxUpdate { request_id, params } => { + self.inbox_update(to_connection_request_id(request_id), params) + .await; + } ClientRequest::SkillsConfigWrite { request_id, params } => { self.skills_config_write(to_connection_request_id(request_id), params) .await; @@ -5201,6 +5214,50 @@ impl CodexMessageProcessor { }); } + async fn inbox_list(&self, request_id: ConnectionRequestId, params: InboxListParams) { + let InboxListParams {} = params; + + match inbox_list_helpers::load_inbox_entries(&self.config.codex_home).await { + Ok(data) => { + self.outgoing + .send_response(request_id, InboxListResponse { data }) + .await; + } + Err(err) => { + self.send_internal_error(request_id, format!("failed to list inbox: {err}")) + .await; + } + } + } + + async fn inbox_update(&self, request_id: ConnectionRequestId, params: InboxUpdateParams) { + let InboxUpdateParams { + thread_id, + last_read_at, + } = params; + + match inbox_list_helpers::update_inbox_entry_last_read_at( + &self.config.codex_home, + &thread_id, + &last_read_at, + ) + .await + { + Ok(entry) => { + self.outgoing + .send_response(request_id, InboxUpdateResponse { entry }) + .await; + } + Err(inbox_list_helpers::InboxUpdateError::InvalidRequest(message)) => { + self.send_invalid_request_error(request_id, message).await; + } + Err(inbox_list_helpers::InboxUpdateError::Io(err)) => { + self.send_internal_error(request_id, format!("failed to update inbox: {err}")) + .await; + } + } + } + async fn apps_list_task( outgoing: Arc, request_id: ConnectionRequestId, diff --git a/codex-rs/app-server/src/codex_message_processor/inbox_list_helpers.rs b/codex-rs/app-server/src/codex_message_processor/inbox_list_helpers.rs new file mode 100644 index 0000000000..800e739138 --- /dev/null +++ b/codex-rs/app-server/src/codex_message_processor/inbox_list_helpers.rs @@ -0,0 +1,377 @@ +use std::io; +use std::path::Path; + +use chrono::DateTime; +use serde_json::Map; +use serde_json::Value; +use serde_json::json; +use tokio::fs; +use tracing::warn; + +#[derive(Debug)] +pub(super) enum InboxUpdateError { + InvalidRequest(String), + Io(io::Error), +} + +impl From for InboxUpdateError { + fn from(value: io::Error) -> Self { + Self::Io(value) + } +} + +pub(super) async fn load_inbox_entries(codex_home: &Path) -> io::Result> { + let inbox_dir = codex_home.join("inbox"); + let mut dir = match fs::read_dir(&inbox_dir).await { + Ok(dir) => dir, + Err(err) if err.kind() == io::ErrorKind::NotFound => { + return Ok(Vec::new()); + } + Err(err) => { + return Err(err); + } + }; + + let user_tracking_by_thread = match load_user_tracking_by_thread(codex_home).await { + Ok(user_tracking_by_thread) => user_tracking_by_thread, + Err(err) => { + warn!("Skipping malformed inbox tracking file: {err}"); + Map::new() + } + }; + + let mut paths = Vec::new(); + while let Some(entry) = dir.next_entry().await? { + let file_name = entry.file_name(); + let Some(file_name) = file_name.to_str() else { + continue; + }; + + if file_name.starts_with("thread__") && file_name.ends_with(".json") { + paths.push((file_name.to_owned(), entry.path())); + } + } + + paths.sort_by(|a, b| b.0.cmp(&a.0)); + + let mut data = Vec::with_capacity(paths.len()); + for (file_name, path) in paths { + let contents = fs::read_to_string(&path).await?; + match serde_json::from_str::(&contents) { + Ok(mut value) => { + remove_thread_owned_last_read_at(&mut value); + apply_user_tracking(&mut value, user_tracking_by_thread.get(&file_name)); + data.push(value); + } + Err(err) => { + warn!("Skipping malformed inbox entry {file_name}: {err}"); + } + } + } + + Ok(data) +} + +pub(super) async fn update_inbox_entry_last_read_at( + codex_home: &Path, + thread_id: &str, + last_read_at: &str, +) -> Result { + if thread_id.is_empty() { + return Err(InboxUpdateError::InvalidRequest( + "thread_id must not be empty".to_string(), + )); + } + + if thread_id.contains(['/', '\\']) { + return Err(InboxUpdateError::InvalidRequest(format!( + "thread_id must be a filename, not a path: {thread_id}" + ))); + } + + if DateTime::parse_from_rfc3339(last_read_at).is_err() { + return Err(InboxUpdateError::InvalidRequest(format!( + "last_read_at must be an RFC 3339 timestamp: {last_read_at}" + ))); + } + + let thread_path = codex_home.join("inbox").join(thread_id); + if let Err(err) = fs::metadata(&thread_path).await { + if err.kind() == io::ErrorKind::NotFound { + return Err(InboxUpdateError::InvalidRequest(format!( + "inbox entry not found: {thread_id}" + ))); + } + + return Err(err.into()); + } + + let mut user_tracking_by_thread = match load_user_tracking_by_thread(codex_home).await { + Ok(user_tracking_by_thread) => user_tracking_by_thread, + Err(err) => { + return Err(InboxUpdateError::InvalidRequest(format!( + "inbox tracking file is not valid JSON: {err}" + ))); + } + }; + + let user_tracking = user_tracking_by_thread + .entry(thread_id.to_string()) + .or_insert_with(|| json!({})); + let Some(user_tracking_object) = user_tracking.as_object_mut() else { + return Err(InboxUpdateError::InvalidRequest(format!( + "tracking entry for {thread_id} must be a JSON object" + ))); + }; + user_tracking_object.insert("last_read_at".to_string(), json!(last_read_at)); + + let tracking_path = codex_home.join("inbox").join("tracking.json"); + let serialized = serde_json::to_string_pretty(&user_tracking_by_thread).map_err(|err| { + InboxUpdateError::InvalidRequest(format!("failed to serialize tracking file: {err}")) + })?; + fs::write(&tracking_path, format!("{serialized}\n")).await?; + + let contents = match fs::read_to_string(&thread_path).await { + Ok(contents) => contents, + Err(err) if err.kind() == io::ErrorKind::NotFound => { + return Err(InboxUpdateError::InvalidRequest(format!( + "inbox entry not found: {thread_id}" + ))); + } + Err(err) => { + return Err(err.into()); + } + }; + + let mut entry = serde_json::from_str::(&contents).map_err(|err| { + InboxUpdateError::InvalidRequest(format!("inbox entry is not valid JSON: {err}")) + })?; + + remove_thread_owned_last_read_at(&mut entry); + apply_user_tracking(&mut entry, user_tracking_by_thread.get(thread_id)); + + Ok(entry) +} + +async fn load_user_tracking_by_thread(codex_home: &Path) -> Result, String> { + let path = codex_home.join("inbox").join("tracking.json"); + let contents = match fs::read_to_string(path).await { + Ok(contents) => contents, + Err(err) if err.kind() == io::ErrorKind::NotFound => { + return Ok(Map::new()); + } + Err(err) => { + return Err(err.to_string()); + } + }; + + serde_json::from_str::>(&contents).map_err(|err| err.to_string()) +} + +fn apply_user_tracking(entry: &mut Value, user_tracking: Option<&Value>) { + let Some(user_tracking) = user_tracking else { + return; + }; + let Some(user_tracking_object) = user_tracking.as_object() else { + return; + }; + let Some(entry_object) = entry.as_object_mut() else { + return; + }; + + let tracking = entry_object + .entry("tracking".to_string()) + .or_insert_with(|| json!({})); + let Some(tracking_object) = tracking.as_object_mut() else { + return; + }; + + for (key, value) in user_tracking_object { + tracking_object.insert(key.clone(), value.clone()); + } +} + +fn remove_thread_owned_last_read_at(entry: &mut Value) { + let Some(entry_object) = entry.as_object_mut() else { + return; + }; + let Some(tracking) = entry_object.get_mut("tracking") else { + return; + }; + let Some(tracking_object) = tracking.as_object_mut() else { + return; + }; + + tracking_object.remove("last_read_at"); +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + use pretty_assertions::assert_eq; + use serde_json::json; + use tempfile::TempDir; + + #[tokio::test] + async fn load_inbox_entries_returns_newest_first_and_skips_malformed_files() -> Result<()> { + let tempdir = TempDir::new()?; + let inbox_dir = tempdir.path().join("inbox"); + fs::create_dir(&inbox_dir).await?; + + fs::write( + inbox_dir.join("thread__20260319T202114Z__slack__C1__111.000000.json"), + json!({ + "schema_version": 1, + "current_progress": "older" + }) + .to_string(), + ) + .await?; + fs::write( + inbox_dir.join("thread__20260320T012640Z__slack__C2__222.000000.json"), + json!({ + "schema_version": 1, + "current_progress": "newer", + "tracking": { + "last_read_at": "2026-03-19T16:20:00-07:00" + } + }) + .to_string(), + ) + .await?; + fs::write( + inbox_dir.join("thread__20260320T020000Z__slack__C3__333.000000.json"), + "{", + ) + .await?; + fs::write(inbox_dir.join("notes.txt"), "ignored").await?; + fs::write( + inbox_dir.join("tracking.json"), + json!({ + "thread__20260320T012640Z__slack__C2__222.000000.json": { + "last_read_at": "2026-03-20T09:15:00-07:00" + } + }) + .to_string(), + ) + .await?; + + let entries = load_inbox_entries(tempdir.path()).await?; + + assert_eq!( + entries, + vec![ + json!({ + "schema_version": 1, + "current_progress": "newer", + "tracking": { + "last_read_at": "2026-03-20T09:15:00-07:00" + } + }), + json!({ + "schema_version": 1, + "current_progress": "older" + }), + ] + ); + Ok(()) + } + + #[tokio::test] + async fn load_inbox_entries_returns_empty_when_inbox_dir_is_missing() -> Result<()> { + let tempdir = TempDir::new()?; + + let entries = load_inbox_entries(tempdir.path()).await?; + + assert_eq!(entries, Vec::::new()); + Ok(()) + } + + #[tokio::test] + async fn update_inbox_entry_last_read_at_writes_tracking_file_and_leaves_thread_file_unchanged() + -> Result<()> { + let tempdir = TempDir::new()?; + let inbox_dir = tempdir.path().join("inbox"); + fs::create_dir(&inbox_dir).await?; + let thread_id = "thread__20260320T012640Z__slack__C08MGJXUCUQ__1773969612.866769.json"; + let thread_path = inbox_dir.join(thread_id); + let original_thread_entry = json!({ + "schema_version": 1, + "source": { + "type": "slack", + "channel_id": "C08MGJXUCUQ", + "thread_ts": "1773969612.866769" + }, + "tracking": { + "last_refreshed_at": "2026-03-19T18:54:47-07:00" + }, + "timeline": [] + }); + fs::write(&thread_path, original_thread_entry.to_string()).await?; + + let entry = + update_inbox_entry_last_read_at(tempdir.path(), thread_id, "2026-03-20T09:15:00-07:00") + .await + .expect("update succeeds"); + + assert_eq!( + entry, + json!({ + "schema_version": 1, + "source": { + "type": "slack", + "channel_id": "C08MGJXUCUQ", + "thread_ts": "1773969612.866769" + }, + "tracking": { + "last_read_at": "2026-03-20T09:15:00-07:00", + "last_refreshed_at": "2026-03-19T18:54:47-07:00" + }, + "timeline": [] + }) + ); + + let reloaded_thread = + serde_json::from_str::(&fs::read_to_string(&thread_path).await?)?; + assert_eq!(reloaded_thread, original_thread_entry); + + let reloaded_tracking = serde_json::from_str::( + &fs::read_to_string(inbox_dir.join("tracking.json")).await?, + )?; + assert_eq!( + reloaded_tracking, + json!({ + thread_id: { + "last_read_at": "2026-03-20T09:15:00-07:00" + } + }) + ); + Ok(()) + } + + #[tokio::test] + async fn update_inbox_entry_last_read_at_rejects_path_traversal() -> Result<()> { + let tempdir = TempDir::new()?; + + let err = update_inbox_entry_last_read_at( + tempdir.path(), + "../thread__20260320T012640Z__slack__C08MGJXUCUQ__1773969612.866769.json", + "2026-03-20T09:15:00-07:00", + ) + .await + .expect_err("path-like ids are rejected"); + + match err { + InboxUpdateError::InvalidRequest(message) => { + assert_eq!( + message, + "thread_id must be a filename, not a path: ../thread__20260320T012640Z__slack__C08MGJXUCUQ__1773969612.866769.json" + ); + } + InboxUpdateError::Io(err) => panic!("unexpected io error: {err}"), + } + + Ok(()) + } +}