Compare commits

...

1 Commits

Author SHA1 Message Date
Matthew Zeng
da838ef284 update 2026-03-20 11:17:55 -07:00
18 changed files with 896 additions and 1 deletions

View File

@@ -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": {

View File

@@ -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": [

View File

@@ -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": {

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"entry": true
},
"required": [
"entry"
],
"title": "InboxUpdateResponse",
"type": "object"
}

View File

@@ -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, };

View File

@@ -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<string, never>;

View File

@@ -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<JsonValue>, };

View File

@@ -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, };

View File

@@ -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, };

View File

@@ -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";

View File

@@ -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,

View File

@@ -2061,6 +2061,40 @@ pub struct AppsListResponse {
pub next_cursor: Option<String>,
}
#[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<serde_json::Value>,
}
#[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/")]

View File

@@ -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 `$<app-slug>` 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://<connector-id>` path rather than guessing by name. Plugins use the same `mention` item shape, but with `plugin://<plugin-name>@<marketplace-name>` paths from `plugin/list`.
Example:

View File

@@ -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<OutgoingMessageSender>,
request_id: ConnectionRequestId,

View File

@@ -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<io::Error> for InboxUpdateError {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}
pub(super) async fn load_inbox_entries(codex_home: &Path) -> io::Result<Vec<Value>> {
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::<Value>(&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<Value, InboxUpdateError> {
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::<Value>(&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<Map<String, Value>, 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::<Map<String, Value>>(&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::<Value>::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::<Value>(&fs::read_to_string(&thread_path).await?)?;
assert_eq!(reloaded_thread, original_thread_entry);
let reloaded_tracking = serde_json::from_str::<Value>(
&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(())
}
}