From 5edcf1ce8c8537e371f7fa7f96133b4a43617bc8 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 16 Mar 2026 17:38:29 +0000 Subject: [PATCH 1/7] feat: add memory citation to agent message --- .../schema/json/ServerNotification.json | 59 +++++++++++++ .../codex_app_server_protocol.schemas.json | 59 +++++++++++++ .../codex_app_server_protocol.v2.schemas.json | 59 +++++++++++++ .../json/v2/ItemCompletedNotification.json | 59 +++++++++++++ .../json/v2/ItemStartedNotification.json | 59 +++++++++++++ .../schema/json/v2/ReviewStartResponse.json | 59 +++++++++++++ .../schema/json/v2/ThreadForkResponse.json | 59 +++++++++++++ .../schema/json/v2/ThreadListResponse.json | 59 +++++++++++++ .../json/v2/ThreadMetadataUpdateResponse.json | 59 +++++++++++++ .../schema/json/v2/ThreadReadResponse.json | 59 +++++++++++++ .../schema/json/v2/ThreadResumeResponse.json | 59 +++++++++++++ .../json/v2/ThreadRollbackResponse.json | 59 +++++++++++++ .../schema/json/v2/ThreadStartResponse.json | 59 +++++++++++++ .../json/v2/ThreadStartedNotification.json | 59 +++++++++++++ .../json/v2/ThreadUnarchiveResponse.json | 59 +++++++++++++ .../json/v2/TurnCompletedNotification.json | 59 +++++++++++++ .../schema/json/v2/TurnStartResponse.json | 59 +++++++++++++ .../json/v2/TurnStartedNotification.json | 59 +++++++++++++ .../schema/typescript/v2/MemoryCitation.ts | 6 ++ .../typescript/v2/MemoryCitationEntry.ts | 5 ++ .../schema/typescript/v2/ThreadItem.ts | 3 +- .../schema/typescript/v2/index.ts | 2 + .../src/protocol/thread_history.rs | 45 ++++++++-- .../app-server-protocol/src/protocol/v2.rs | 63 ++++++++++++++ codex-rs/app-server/README.md | 4 +- .../tests/suite/v2/thread_resume.rs | 1 + codex-rs/core/src/codex.rs | 1 + codex-rs/core/src/event_mapping.rs | 7 +- codex-rs/core/src/memories/citations.rs | 85 +++++++++++++++---- codex-rs/core/src/memories/citations_tests.rs | 38 +++++++++ codex-rs/core/src/rollout/recorder_tests.rs | 3 + codex-rs/core/src/stream_events_utils.rs | 21 ++++- .../core/src/stream_events_utils_tests.rs | 13 ++- codex-rs/core/src/turn_timing_tests.rs | 2 + codex-rs/protocol/src/items.rs | 6 ++ codex-rs/protocol/src/lib.rs | 1 + codex-rs/protocol/src/memory_citation.rs | 20 +++++ codex-rs/protocol/src/protocol.rs | 3 + 38 files changed, 1363 insertions(+), 28 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/MemoryCitation.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/MemoryCitationEntry.ts create mode 100644 codex-rs/protocol/src/memory_citation.rs diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 7303fa1ca7c..936e02dcc86 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -1454,6 +1454,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -2173,6 +2221,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { 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 bfb716c9976..855f23771ad 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 @@ -8663,6 +8663,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/v2/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MergeStrategy": { "enum": [ "replace", @@ -11985,6 +12033,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/v2/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { 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 b726a187647..dd9aac68010 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 @@ -5451,6 +5451,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MergeStrategy": { "enum": [ "replace", @@ -9745,6 +9793,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index b447fc3397a..4bc6ba2ce80 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -289,6 +289,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -444,6 +492,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index 1b02f44188e..cbee3c60ae5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -289,6 +289,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -444,6 +492,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index b8e83ba34e5..5bb05e2b1e5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -403,6 +403,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -558,6 +606,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index e57c84b4639..d0dece52526 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -488,6 +488,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -1038,6 +1086,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index f9f94305501..3ed402c271e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -426,6 +426,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -796,6 +844,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index c652c1cb447..f2d492407f2 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -426,6 +426,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -796,6 +844,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index b0ca838cdec..dd105891db1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -426,6 +426,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -796,6 +844,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 6809f9715bc..0f7371c067b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -488,6 +488,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -1038,6 +1086,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 2288caa5081..9c1681c987b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -426,6 +426,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -796,6 +844,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index e994a2b009a..e01803c994c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -488,6 +488,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -1038,6 +1086,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 3eabf9eebc8..6217a9cebee 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -426,6 +426,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -796,6 +844,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index f6738ff216d..992e9b1d710 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -426,6 +426,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -796,6 +844,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 079a81ad047..a1d864ef03d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -403,6 +403,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -558,6 +606,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 17f04c51d8a..bdaceed51bb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -403,6 +403,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -558,6 +606,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 59171e42d06..c4deb836b80 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -403,6 +403,54 @@ ], "type": "string" }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "rolloutIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "rolloutIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, "MessagePhase": { "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", "oneOf": [ @@ -558,6 +606,17 @@ "id": { "type": "string" }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, "phase": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/MemoryCitation.ts b/codex-rs/app-server-protocol/schema/typescript/v2/MemoryCitation.ts new file mode 100644 index 00000000000..4760e0bc892 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/MemoryCitation.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 { MemoryCitationEntry } from "./MemoryCitationEntry"; + +export type MemoryCitation = { entries: Array, rolloutIds: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/MemoryCitationEntry.ts b/codex-rs/app-server-protocol/schema/typescript/v2/MemoryCitationEntry.ts new file mode 100644 index 00000000000..9b9ce17267f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/MemoryCitationEntry.ts @@ -0,0 +1,5 @@ +// 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. + +export type MemoryCitationEntry = { path: string, lineStart: number, lineEnd: number, note: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index bcc81c02515..51ab9e88122 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -15,11 +15,12 @@ import type { FileUpdateChange } from "./FileUpdateChange"; import type { McpToolCallError } from "./McpToolCallError"; import type { McpToolCallResult } from "./McpToolCallResult"; import type { McpToolCallStatus } from "./McpToolCallStatus"; +import type { MemoryCitation } from "./MemoryCitation"; import type { PatchApplyStatus } from "./PatchApplyStatus"; import type { UserInput } from "./UserInput"; import type { WebSearchAction } from "./WebSearchAction"; -export type ThreadItem = { "type": "userMessage", id: string, content: Array, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array, content: Array, } | { "type": "commandExecution", id: string, +export type ThreadItem = { "type": "userMessage", id: string, content: Array, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array, content: Array, } | { "type": "commandExecution", id: string, /** * The command to be executed. */ 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 b8419a79dde..d7b8571331b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -174,6 +174,8 @@ export type { McpToolCallError } from "./McpToolCallError"; export type { McpToolCallProgressNotification } from "./McpToolCallProgressNotification"; export type { McpToolCallResult } from "./McpToolCallResult"; export type { McpToolCallStatus } from "./McpToolCallStatus"; +export type { MemoryCitation } from "./MemoryCitation"; +export type { MemoryCitationEntry } from "./MemoryCitationEntry"; export type { MergeStrategy } from "./MergeStrategy"; export type { Model } from "./Model"; export type { ModelAvailabilityNux } from "./ModelAvailabilityNux"; diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index 6a570b7fa5b..ef1b2fb26fb 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -118,9 +118,11 @@ impl ThreadHistoryBuilder { pub fn handle_event(&mut self, event: &EventMsg) { match event { EventMsg::UserMessage(payload) => self.handle_user_message(payload), - EventMsg::AgentMessage(payload) => { - self.handle_agent_message(payload.message.clone(), payload.phase.clone()) - } + EventMsg::AgentMessage(payload) => self.handle_agent_message( + payload.message.clone(), + payload.phase.clone(), + payload.memory_citation.clone().map(Into::into), + ), EventMsg::AgentReasoning(payload) => self.handle_agent_reasoning(payload), EventMsg::AgentReasoningRawContent(payload) => { self.handle_agent_reasoning_raw_content(payload) @@ -208,15 +210,23 @@ impl ThreadHistoryBuilder { self.current_turn = Some(turn); } - fn handle_agent_message(&mut self, text: String, phase: Option) { + fn handle_agent_message( + &mut self, + text: String, + phase: Option, + memory_citation: Option, + ) { if text.is_empty() { return; } let id = self.next_item_id(); - self.ensure_turn() - .items - .push(ThreadItem::AgentMessage { id, text, phase }); + self.ensure_turn().items.push(ThreadItem::AgentMessage { + id, + text, + phase, + memory_citation, + }); } fn handle_agent_reasoning(&mut self, payload: &AgentReasoningEvent) { @@ -1178,6 +1188,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "Hi there".into(), phase: None, + memory_citation: None, }), EventMsg::AgentReasoning(AgentReasoningEvent { text: "thinking".into(), @@ -1194,6 +1205,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "Reply two".into(), phase: None, + memory_citation: None, }), ]; @@ -1229,6 +1241,7 @@ mod tests { id: "item-2".into(), text: "Hi there".into(), phase: None, + memory_citation: None, } ); assert_eq!( @@ -1260,6 +1273,7 @@ mod tests { id: "item-5".into(), text: "Reply two".into(), phase: None, + memory_citation: None, } ); } @@ -1318,6 +1332,7 @@ mod tests { let events = vec![EventMsg::AgentMessage(AgentMessageEvent { message: "Final reply".into(), phase: Some(CoreMessagePhase::FinalAnswer), + memory_citation: None, })]; let items = events @@ -1332,6 +1347,7 @@ mod tests { id: "item-1".into(), text: "Final reply".into(), phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, } ); } @@ -1354,6 +1370,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "interlude".into(), phase: None, + memory_citation: None, }), EventMsg::AgentReasoning(AgentReasoningEvent { text: "second summary".into(), @@ -1399,6 +1416,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "Working...".into(), phase: None, + memory_citation: None, }), EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some("turn-1".into()), @@ -1413,6 +1431,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "Second attempt complete.".into(), phase: None, + memory_citation: None, }), ]; @@ -1442,6 +1461,7 @@ mod tests { id: "item-2".into(), text: "Working...".into(), phase: None, + memory_citation: None, } ); @@ -1464,6 +1484,7 @@ mod tests { id: "item-4".into(), text: "Second attempt complete.".into(), phase: None, + memory_citation: None, } ); } @@ -1480,6 +1501,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "A1".into(), phase: None, + memory_citation: None, }), EventMsg::UserMessage(UserMessageEvent { message: "Second".into(), @@ -1490,6 +1512,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "A2".into(), phase: None, + memory_citation: None, }), EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }), EventMsg::UserMessage(UserMessageEvent { @@ -1501,6 +1524,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "A3".into(), phase: None, + memory_citation: None, }), ]; @@ -1529,6 +1553,7 @@ mod tests { id: "item-2".into(), text: "A1".into(), phase: None, + memory_citation: None, }, ] ); @@ -1546,6 +1571,7 @@ mod tests { id: "item-4".into(), text: "A3".into(), phase: None, + memory_citation: None, }, ] ); @@ -1563,6 +1589,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "A1".into(), phase: None, + memory_citation: None, }), EventMsg::UserMessage(UserMessageEvent { message: "Two".into(), @@ -1573,6 +1600,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "A2".into(), phase: None, + memory_citation: None, }), EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 99 }), ]; @@ -2209,6 +2237,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "still in b".into(), phase: None, + memory_citation: None, }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-b".into(), @@ -2263,6 +2292,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "still in b".into(), phase: None, + memory_citation: None, }), ]; @@ -2497,6 +2527,7 @@ mod tests { EventMsg::AgentMessage(AgentMessageEvent { message: "done".into(), phase: None, + memory_citation: None, }), EventMsg::Error(ErrorEvent { message: "rollback failed".into(), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index c5546f49b71..fde01b00b89 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -30,6 +30,8 @@ use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::mcp::Resource as McpResource; use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; use codex_protocol::mcp::Tool as McpTool; +use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation; +use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry; use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; use codex_protocol::models::MacOsAutomationPermission as CoreMacOsAutomationPermission; use codex_protocol::models::MacOsContactsPermission as CoreMacOsContactsPermission; @@ -3620,6 +3622,44 @@ pub struct Turn { pub error: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MemoryCitation { + pub entries: Vec, + pub rollout_ids: Vec, +} + +impl From for MemoryCitation { + fn from(value: CoreMemoryCitation) -> Self { + Self { + entries: value.entries.into_iter().map(Into::into).collect(), + rollout_ids: value.rollout_ids, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MemoryCitationEntry { + pub path: String, + pub line_start: u32, + pub line_end: u32, + pub note: String, +} + +impl From for MemoryCitationEntry { + fn from(value: CoreMemoryCitationEntry) -> Self { + Self { + path: value.path, + line_start: value.line_start, + line_end: value.line_end, + note: value.note, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Error)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -4114,6 +4154,8 @@ pub enum ThreadItem { text: String, #[serde(default)] phase: Option, + #[serde(default)] + memory_citation: Option, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -4363,6 +4405,7 @@ impl From for ThreadItem { id: agent.id, text, phase: agent.phase, + memory_citation: agent.memory_citation.map(Into::into), } } CoreTurnItem::Plan(plan) => ThreadItem::Plan { @@ -7439,6 +7482,7 @@ mod tests { }, ], phase: None, + memory_citation: None, }); assert_eq!( @@ -7447,6 +7491,7 @@ mod tests { id: "agent-1".to_string(), text: "Hello world".to_string(), phase: None, + memory_citation: None, } ); @@ -7456,6 +7501,15 @@ mod tests { text: "final".to_string(), }], phase: Some(MessagePhase::FinalAnswer), + memory_citation: Some(CoreMemoryCitation { + entries: vec![CoreMemoryCitationEntry { + path: "MEMORY.md".to_string(), + line_start: 1, + line_end: 2, + note: "summary".to_string(), + }], + rollout_ids: vec!["rollout-1".to_string()], + }), }); assert_eq!( @@ -7464,6 +7518,15 @@ mod tests { id: "agent-2".to_string(), text: "final".to_string(), phase: Some(MessagePhase::FinalAnswer), + memory_citation: Some(MemoryCitation { + entries: vec![MemoryCitationEntry { + path: "MEMORY.md".to_string(), + line_start: 1, + line_end: 2, + note: "summary".to_string(), + }], + rollout_ids: vec!["rollout-1".to_string()], + }), } ); diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 67bc310da13..c3e9db35ca3 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -330,7 +330,7 @@ If this was the last subscriber, the server unloads the thread and emits `thread ### Example: Read a thread -Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want the rollout history loaded into `thread.turns`. The returned thread includes `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available. +Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want the rollout history loaded into `thread.turns`. The returned thread includes `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available. Assistant `ThreadItem::AgentMessage` items can also include `memoryCitation` metadata on final answers when the underlying response carried a parsed memory citation block. ```json { "method": "thread/read", "id": 22, "params": { "threadId": "thr_123" } } @@ -770,6 +770,8 @@ Event notifications are the server-initiated event stream for thread lifecycles, Thread realtime uses a separate thread-scoped notification surface. `thread/realtime/*` notifications are ephemeral transport events, not `ThreadItem`s, and are not returned by `thread/read`, `thread/resume`, or `thread/fork`. +Completed assistant `item/*` notifications reuse the same `ThreadItem::AgentMessage` shape as persisted history, including optional `memoryCitation` metadata for final answers. + ### Notification opt-out Clients can suppress specific notifications per connection by sending exact method names in `initialize.params.capabilities.optOutNotificationMethods`. diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 27803db42cc..5cbcd3b25d2 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -450,6 +450,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is "payload": serde_json::to_value(EventMsg::AgentMessage(AgentMessageEvent { message: "Still running".to_string(), phase: None, + memory_citation: None, }))?, }) .to_string(), diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index d30e5a3eafa..05ec686697f 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -6983,6 +6983,7 @@ async fn emit_agent_message_in_plan_mode( id: agent_message_id.clone(), content: Vec::new(), phase: None, + memory_citation: None, }) }); sess.emit_turn_item_started(turn_context, &start_item).await; diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index 72372b24cd8..7a9cdb39063 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -83,7 +83,12 @@ fn parse_agent_message( } } let id = id.cloned().unwrap_or_else(|| Uuid::new_v4().to_string()); - AgentMessageItem { id, content, phase } + AgentMessageItem { + id, + content, + phase, + memory_citation: None, + } } pub fn parse_turn_item(item: &ResponseItem) -> Option { diff --git a/codex-rs/core/src/memories/citations.rs b/codex-rs/core/src/memories/citations.rs index ed620e853b5..d8642880f1b 100644 --- a/codex-rs/core/src/memories/citations.rs +++ b/codex-rs/core/src/memories/citations.rs @@ -1,36 +1,89 @@ use codex_protocol::ThreadId; +use codex_protocol::memory_citation::MemoryCitation; +use codex_protocol::memory_citation::MemoryCitationEntry; +use std::collections::HashSet; + +pub fn parse_memory_citation(citations: Vec) -> Option { + let mut entries = Vec::new(); + let mut rollout_ids = Vec::new(); + let mut seen_rollout_ids = HashSet::new(); -pub fn get_thread_id_from_citations(citations: Vec) -> Vec { - let mut result = Vec::new(); for citation in citations { - let mut ids_block = None; - for (open, close) in [ - ("", ""), - ("", ""), - ] { - if let Some((_, rest)) = citation.split_once(open) - && let Some((ids, _)) = rest.split_once(close) - { - ids_block = Some(ids); - break; - } + if let Some(entries_block) = + extract_block(&citation, "", "") + { + entries.extend( + entries_block + .lines() + .filter_map(parse_memory_citation_entry), + ); } - if let Some(ids_block) = ids_block { + if let Some(ids_block) = extract_ids_block(&citation) { for id in ids_block .lines() .map(str::trim) .filter(|line| !line.is_empty()) { - if let Ok(thread_id) = ThreadId::try_from(id) { - result.push(thread_id); + if seen_rollout_ids.insert(id.to_string()) { + rollout_ids.push(id.to_string()); } } } } + + if entries.is_empty() && rollout_ids.is_empty() { + None + } else { + Some(MemoryCitation { + entries, + rollout_ids, + }) + } +} + +pub fn get_thread_id_from_citations(citations: Vec) -> Vec { + let mut result = Vec::new(); + if let Some(memory_citation) = parse_memory_citation(citations) { + for rollout_id in memory_citation.rollout_ids { + if let Ok(thread_id) = ThreadId::try_from(rollout_id.as_str()) { + result.push(thread_id); + } + } + } result } +fn parse_memory_citation_entry(line: &str) -> Option { + let line = line.trim(); + if line.is_empty() { + return None; + } + + let (location, note) = line.rsplit_once("|note=[")?; + let note = note.strip_suffix(']')?.trim().to_string(); + let (path, line_range) = location.rsplit_once(':')?; + let (line_start, line_end) = line_range.split_once('-')?; + + Some(MemoryCitationEntry { + path: path.trim().to_string(), + line_start: line_start.trim().parse().ok()?, + line_end: line_end.trim().parse().ok()?, + note, + }) +} + +fn extract_block<'a>(text: &'a str, open: &str, close: &str) -> Option<&'a str> { + let (_, rest) = text.split_once(open)?; + let (body, _) = rest.split_once(close)?; + Some(body) +} + +fn extract_ids_block(text: &str) -> Option<&str> { + extract_block(text, "", "") + .or_else(|| extract_block(text, "", "")) +} + #[cfg(test)] #[path = "citations_tests.rs"] mod tests; diff --git a/codex-rs/core/src/memories/citations_tests.rs b/codex-rs/core/src/memories/citations_tests.rs index b6783dea7cf..49d4a674307 100644 --- a/codex-rs/core/src/memories/citations_tests.rs +++ b/codex-rs/core/src/memories/citations_tests.rs @@ -1,4 +1,5 @@ use super::get_thread_id_from_citations; +use super::parse_memory_citation; use codex_protocol::ThreadId; use pretty_assertions::assert_eq; @@ -24,3 +25,40 @@ fn get_thread_id_from_citations_supports_legacy_rollout_ids() { assert_eq!(get_thread_id_from_citations(citations), vec![thread_id]); } + +#[test] +fn parse_memory_citation_extracts_entries_and_rollout_ids() { + let first = ThreadId::new(); + let second = ThreadId::new(); + let citations = vec![format!( + "\nMEMORY.md:1-2|note=[summary]\nrollout_summaries/foo.md:10-12|note=[details]\n\n\n{first}\n{second}\n{first}\n" + )]; + + let parsed = parse_memory_citation(citations).expect("memory citation should parse"); + + assert_eq!( + parsed + .entries + .iter() + .map(|entry| ( + entry.path.clone(), + entry.line_start, + entry.line_end, + entry.note.clone(), + )) + .collect::>(), + vec![ + ("MEMORY.md".to_string(), 1, 2, "summary".to_string()), + ( + "rollout_summaries/foo.md".to_string(), + 10, + 12, + "details".to_string() + ), + ] + ); + assert_eq!( + parsed.rollout_ids, + vec![first.to_string(), second.to_string()] + ); +} diff --git a/codex-rs/core/src/rollout/recorder_tests.rs b/codex-rs/core/src/rollout/recorder_tests.rs index f6f588574a7..dbe11ac9f79 100644 --- a/codex-rs/core/src/rollout/recorder_tests.rs +++ b/codex-rs/core/src/rollout/recorder_tests.rs @@ -85,6 +85,7 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result< AgentMessageEvent { message: "buffered-event".to_string(), phase: None, + memory_citation: None, }, ))]) .await?; @@ -201,6 +202,7 @@ async fn metadata_irrelevant_events_touch_state_db_updated_at() -> std::io::Resu AgentMessageEvent { message: "assistant text".to_string(), phase: None, + memory_citation: None, }, ))]) .await?; @@ -251,6 +253,7 @@ async fn metadata_irrelevant_events_fall_back_to_upsert_when_thread_missing() -> AgentMessageEvent { message: "assistant text".to_string(), phase: None, + memory_citation: None, }, ))]; diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index a44bc01f55d..084cb4b1a36 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -15,6 +15,7 @@ use crate::error::CodexErr; use crate::error::Result; use crate::function_tool::FunctionCallError; use crate::memories::citations::get_thread_id_from_citations; +use crate::memories::citations::parse_memory_citation; use crate::parse_turn_item; use crate::state_db; use crate::tools::parallel::ToolCallRuntime; @@ -38,6 +39,22 @@ fn strip_hidden_assistant_markup(text: &str, plan_mode: bool) -> String { } } +fn strip_hidden_assistant_markup_and_parse_memory_citation( + text: &str, + plan_mode: bool, +) -> ( + String, + Option, +) { + let (without_citations, citations) = strip_citations(text); + let visible_text = if plan_mode { + strip_proposed_plan_blocks(&without_citations) + } else { + without_citations + }; + (visible_text, parse_memory_citation(citations)) +} + pub(crate) fn raw_assistant_output_text_from_item(item: &ResponseItem) -> Option { if let ResponseItem::Message { role, content, .. } = item && role == "assistant" @@ -297,9 +314,11 @@ pub(crate) async fn handle_non_tool_response_item( codex_protocol::items::AgentMessageContent::Text { text } => text.as_str(), }) .collect::(); - let stripped = strip_hidden_assistant_markup(&combined, plan_mode); + let (stripped, memory_citation) = + strip_hidden_assistant_markup_and_parse_memory_citation(&combined, plan_mode); agent_message.content = vec![codex_protocol::items::AgentMessageContent::Text { text: stripped }]; + agent_message.memory_citation = memory_citation; } if let TurnItem::ImageGeneration(image_item) = &mut turn_item { match save_image_generation_result(&image_item.id, &image_item.result).await { diff --git a/codex-rs/core/src/stream_events_utils_tests.rs b/codex-rs/core/src/stream_events_utils_tests.rs index bfebb8902c5..389f01ec716 100644 --- a/codex-rs/core/src/stream_events_utils_tests.rs +++ b/codex-rs/core/src/stream_events_utils_tests.rs @@ -23,7 +23,9 @@ fn assistant_output_text(text: &str) -> ResponseItem { #[tokio::test] async fn handle_non_tool_response_item_strips_citations_from_assistant_message() { let (session, turn_context) = make_session_and_context().await; - let item = assistant_output_text("hellodoc1 world"); + let item = assistant_output_text( + "hello\nMEMORY.md:1-2|note=[x]\n\n\n019cc2ea-1dff-7902-8d40-c8f6e5d83cc4\n world", + ); let turn_item = handle_non_tool_response_item(&session, &turn_context, &item, false) .await @@ -40,6 +42,15 @@ async fn handle_non_tool_response_item_strips_citations_from_assistant_message() }) .collect::(); assert_eq!(text, "hello world"); + let memory_citation = agent_message + .memory_citation + .expect("memory citation should be parsed"); + assert_eq!(memory_citation.entries.len(), 1); + assert_eq!(memory_citation.entries[0].path, "MEMORY.md"); + assert_eq!( + memory_citation.rollout_ids, + vec!["019cc2ea-1dff-7902-8d40-c8f6e5d83cc4".to_string()] + ); } #[test] diff --git a/codex-rs/core/src/turn_timing_tests.rs b/codex-rs/core/src/turn_timing_tests.rs index 4f292b40dc6..934b6ed30a3 100644 --- a/codex-rs/core/src/turn_timing_tests.rs +++ b/codex-rs/core/src/turn_timing_tests.rs @@ -58,6 +58,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() { id: "msg-1".to_string(), content: Vec::new(), phase: None, + memory_citation: None, })) .await .is_some() @@ -68,6 +69,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() { id: "msg-2".to_string(), content: Vec::new(), phase: None, + memory_citation: None, })) .await, None diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs index f200fe6f752..08e50b9546a 100644 --- a/codex-rs/protocol/src/items.rs +++ b/codex-rs/protocol/src/items.rs @@ -1,3 +1,4 @@ +use crate::memory_citation::MemoryCitation; use crate::models::MessagePhase; use crate::models::WebSearchAction; use crate::protocol::AgentMessageEvent; @@ -58,6 +59,9 @@ pub struct AgentMessageItem { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub phase: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub memory_citation: Option, } #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] @@ -201,6 +205,7 @@ impl AgentMessageItem { id: uuid::Uuid::new_v4().to_string(), content: content.to_vec(), phase: None, + memory_citation: None, } } @@ -211,6 +216,7 @@ impl AgentMessageItem { AgentMessageContent::Text { text } => EventMsg::AgentMessage(AgentMessageEvent { message: text.clone(), phase: self.phase.clone(), + memory_citation: self.memory_citation.clone(), }), }) .collect() diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index d6adf2c5858..08466ba4ea7 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -7,6 +7,7 @@ pub mod custom_prompts; pub mod dynamic_tools; pub mod items; pub mod mcp; +pub mod memory_citation; pub mod message_history; pub mod models; pub mod num_format; diff --git a/codex-rs/protocol/src/memory_citation.rs b/codex-rs/protocol/src/memory_citation.rs new file mode 100644 index 00000000000..6706aea774a --- /dev/null +++ b/codex-rs/protocol/src/memory_citation.rs @@ -0,0 +1,20 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct MemoryCitation { + pub entries: Vec, + pub rollout_ids: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct MemoryCitationEntry { + pub path: String, + pub line_start: u32, + pub line_end: u32, + pub note: String, +} diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index f1f60e163b5..7b5a11bb4ca 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -32,6 +32,7 @@ use crate::mcp::RequestId; use crate::mcp::Resource as McpResource; use crate::mcp::ResourceTemplate as McpResourceTemplate; use crate::mcp::Tool as McpTool; +use crate::memory_citation::MemoryCitation; use crate::message_history::HistoryEntry; use crate::models::BaseInstructions; use crate::models::ContentItem; @@ -1990,6 +1991,8 @@ pub struct AgentMessageEvent { pub message: String, #[serde(default)] pub phase: Option, + #[serde(default)] + pub memory_citation: Option, } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] From 55e755387836101018992ebe1ff68a7fccbd201f Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 16 Mar 2026 17:52:06 +0000 Subject: [PATCH 2/7] fixes --- .../src/app/app_server_adapter.rs | 43 +++++++++++++++---- .../tui_app_server/src/app_server_session.rs | 35 ++++++++++++--- .../tui_app_server/src/chatwidget/tests.rs | 9 ++++ codex-rs/tui_app_server/src/multi_agents.rs | 1 + 4 files changed, 73 insertions(+), 15 deletions(-) diff --git a/codex-rs/tui_app_server/src/app/app_server_adapter.rs b/codex-rs/tui_app_server/src/app/app_server_adapter.rs index 889c4720247..dab79199349 100644 --- a/codex-rs/tui_app_server/src/app/app_server_adapter.rs +++ b/codex-rs/tui_app_server/src/app/app_server_adapter.rs @@ -395,13 +395,33 @@ fn thread_item_to_core(item: ThreadItem) -> Option { .map(codex_app_server_protocol::UserInput::into_core) .collect(), })), - ThreadItem::AgentMessage { id, text, phase } => { - Some(TurnItem::AgentMessage(AgentMessageItem { - id, - content: vec![AgentMessageContent::Text { text }], - phase, - })) - } + ThreadItem::AgentMessage { + id, + text, + phase, + memory_citation, + } => Some(TurnItem::AgentMessage(AgentMessageItem { + id, + content: vec![AgentMessageContent::Text { text }], + phase, + memory_citation: memory_citation.map(|citation| { + codex_protocol::memory_citation::MemoryCitation { + entries: citation + .entries + .into_iter() + .map( + |entry| codex_protocol::memory_citation::MemoryCitationEntry { + path: entry.path, + line_start: entry.line_start, + line_end: entry.line_end, + note: entry.note, + }, + ) + .collect(), + rollout_ids: citation.rollout_ids, + } + }), + })), ThreadItem::Plan { id, text } => Some(TurnItem::Plan(PlanItem { id, text })), ThreadItem::Reasoning { id, @@ -500,6 +520,7 @@ mod tests { id: item_id, text: "Hello from your coding assistant.".to_string(), phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, }, thread_id: thread_id.clone(), turn_id: turn_id.clone(), @@ -524,13 +545,19 @@ mod tests { ); assert_eq!(completed.turn_id, turn_id); match &completed.item { - TurnItem::AgentMessage(AgentMessageItem { id, content, phase }) => { + TurnItem::AgentMessage(AgentMessageItem { + id, + content, + phase, + memory_citation, + }) => { assert_eq!(id, "msg_123"); let [AgentMessageContent::Text { text }] = content.as_slice() else { panic!("expected a single text content item"); }; assert_eq!(text, "Hello from your coding assistant."); assert_eq!(*phase, Some(MessagePhase::FinalAnswer)); + assert_eq!(*memory_citation, None); } _ => panic!("expected bridged agent message item"), } diff --git a/codex-rs/tui_app_server/src/app_server_session.rs b/codex-rs/tui_app_server/src/app_server_session.rs index 19c882caf01..c43181bf810 100644 --- a/codex-rs/tui_app_server/src/app_server_session.rs +++ b/codex-rs/tui_app_server/src/app_server_session.rs @@ -1036,13 +1036,33 @@ fn app_server_thread_item_to_core(item: codex_app_server_protocol::ThreadItem) - .collect(), })) } - codex_app_server_protocol::ThreadItem::AgentMessage { id, text, phase } => { - Some(TurnItem::AgentMessage(AgentMessageItem { - id, - content: vec![AgentMessageContent::Text { text }], - phase, - })) - } + codex_app_server_protocol::ThreadItem::AgentMessage { + id, + text, + phase, + memory_citation, + } => Some(TurnItem::AgentMessage(AgentMessageItem { + id, + content: vec![AgentMessageContent::Text { text }], + phase, + memory_citation: memory_citation.map(|citation| { + codex_protocol::memory_citation::MemoryCitation { + entries: citation + .entries + .into_iter() + .map( + |entry| codex_protocol::memory_citation::MemoryCitationEntry { + path: entry.path, + line_start: entry.line_start, + line_end: entry.line_end, + note: entry.note, + }, + ) + .collect(), + rollout_ids: citation.rollout_ids, + } + }), + })), codex_app_server_protocol::ThreadItem::Plan { id, text } => { Some(TurnItem::Plan(PlanItem { id, text })) } @@ -1237,6 +1257,7 @@ mod tests { id: "assistant-1".to_string(), text: "assistant reply".to_string(), phase: None, + memory_citation: None, }, ], status: TurnStatus::Completed, diff --git a/codex-rs/tui_app_server/src/chatwidget/tests.rs b/codex-rs/tui_app_server/src/chatwidget/tests.rs index 07770182b22..b678a7baad0 100644 --- a/codex-rs/tui_app_server/src/chatwidget/tests.rs +++ b/codex-rs/tui_app_server/src/chatwidget/tests.rs @@ -198,6 +198,7 @@ async fn resumed_initial_messages_render_history() { EventMsg::AgentMessage(AgentMessageEvent { message: "assistant reply".to_string(), phase: None, + memory_citation: None, }), ]), network_proxy: None, @@ -246,6 +247,7 @@ async fn thread_snapshot_replay_does_not_duplicate_agent_message_history() { text: "assistant reply".to_string(), }], phase: None, + memory_citation: None, }), }), }); @@ -254,6 +256,7 @@ async fn thread_snapshot_replay_does_not_duplicate_agent_message_history() { msg: EventMsg::AgentMessage(AgentMessageEvent { message: "assistant reply".to_string(), phase: None, + memory_citation: None, }), }); @@ -1542,6 +1545,7 @@ async fn live_agent_message_renders_during_review_mode() { msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Review progress update".to_string(), phase: None, + memory_citation: None, }), }); @@ -1568,6 +1572,7 @@ async fn thread_snapshot_replay_preserves_agent_message_during_review_mode() { msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Review progress update".to_string(), phase: None, + memory_citation: None, }), }); @@ -3501,6 +3506,7 @@ fn complete_assistant_message( text: text.to_string(), }], phase, + memory_citation: None, }), }), }); @@ -4090,6 +4096,7 @@ async fn live_legacy_agent_message_after_item_completed_does_not_duplicate_assis msg: EventMsg::AgentMessage(AgentMessageEvent { message: "hello".into(), phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, }), }); @@ -5880,6 +5887,7 @@ async fn slash_copy_is_unavailable_when_legacy_agent_message_is_not_repeated_on_ msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Legacy final message".into(), phase: None, + memory_citation: None, }), }); let _ = drain_insert_history(&mut rx); @@ -10721,6 +10729,7 @@ async fn deltas_then_same_final_message_are_rendered_snapshot() { msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Here is the result.".into(), phase: None, + memory_citation: None, }), }); diff --git a/codex-rs/tui_app_server/src/multi_agents.rs b/codex-rs/tui_app_server/src/multi_agents.rs index 00a1c0429cd..1a465d7ada0 100644 --- a/codex-rs/tui_app_server/src/multi_agents.rs +++ b/codex-rs/tui_app_server/src/multi_agents.rs @@ -567,6 +567,7 @@ fn status_summary_spans(status: &AgentStatus) -> Vec> { } spans } + AgentStatus::Interrupted => vec![Span::from("Interrupted").magenta()], AgentStatus::Shutdown => vec![Span::from("Shutdown")], AgentStatus::NotFound => vec![Span::from("Not found").red()], } From 712ddd3a34097e2eeded8c9a701e0936b1be1bff Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 16 Mar 2026 18:52:01 +0000 Subject: [PATCH 3/7] nit fix --- codex-rs/exec/tests/event_processor_with_json_output.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index 63e28f222e1..e9f295337e3 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -749,6 +749,7 @@ fn agent_message_produces_item_completed_agent_message() { EventMsg::AgentMessage(AgentMessageEvent { message: "hello".to_string(), phase: None, + memory_citation: None, }), ); let out = ep.collect_thread_events(&ev); From 1bea9e8d87fdf34aefd485664d79fbd803bb8dd9 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 16 Mar 2026 19:25:40 +0000 Subject: [PATCH 4/7] nit fix --- codex-rs/tui/src/chatwidget/tests.rs | 9 +++++++++ codex-rs/tui_app_server/src/multi_agents.rs | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index ffc571288a3..b7c5486b1fe 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -199,6 +199,7 @@ async fn resumed_initial_messages_render_history() { EventMsg::AgentMessage(AgentMessageEvent { message: "assistant reply".to_string(), phase: None, + memory_citation: None, }), ]), network_proxy: None, @@ -247,6 +248,7 @@ async fn thread_snapshot_replay_does_not_duplicate_agent_message_history() { text: "assistant reply".to_string(), }], phase: None, + memory_citation: None, }), }), }); @@ -255,6 +257,7 @@ async fn thread_snapshot_replay_does_not_duplicate_agent_message_history() { msg: EventMsg::AgentMessage(AgentMessageEvent { message: "assistant reply".to_string(), phase: None, + memory_citation: None, }), }); @@ -1543,6 +1546,7 @@ async fn live_agent_message_renders_during_review_mode() { msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Review progress update".to_string(), phase: None, + memory_citation: None, }), }); @@ -1569,6 +1573,7 @@ async fn thread_snapshot_replay_preserves_agent_message_during_review_mode() { msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Review progress update".to_string(), phase: None, + memory_citation: None, }), }); @@ -3532,6 +3537,7 @@ fn complete_assistant_message( text: text.to_string(), }], phase, + memory_citation: None, }), }), }); @@ -4127,6 +4133,7 @@ async fn live_legacy_agent_message_after_item_completed_does_not_duplicate_assis msg: EventMsg::AgentMessage(AgentMessageEvent { message: "hello".into(), phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, }), }); @@ -5933,6 +5940,7 @@ async fn slash_copy_is_unavailable_when_legacy_agent_message_is_not_repeated_on_ msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Legacy final message".into(), phase: None, + memory_citation: None, }), }); let _ = drain_insert_history(&mut rx); @@ -10684,6 +10692,7 @@ async fn deltas_then_same_final_message_are_rendered_snapshot() { msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Here is the result.".into(), phase: None, + memory_citation: None, }), }); diff --git a/codex-rs/tui_app_server/src/multi_agents.rs b/codex-rs/tui_app_server/src/multi_agents.rs index b8e8ef0cb2a..672e20e1a28 100644 --- a/codex-rs/tui_app_server/src/multi_agents.rs +++ b/codex-rs/tui_app_server/src/multi_agents.rs @@ -570,7 +570,6 @@ fn status_summary_spans(status: &AgentStatus) -> Vec> { } spans } - AgentStatus::Interrupted => vec![Span::from("Interrupted").magenta()], AgentStatus::Shutdown => vec![Span::from("Shutdown")], AgentStatus::NotFound => vec![Span::from("Not found").red()], } From c486c8978fdba7eeffa95ccfdc147650c862c0e4 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Tue, 17 Mar 2026 18:01:27 +0000 Subject: [PATCH 5/7] fix comments --- codex-rs/app-server-protocol/src/protocol/v2.rs | 6 +++--- codex-rs/app-server/README.md | 4 +--- codex-rs/tui_app_server/src/app/app_server_adapter.rs | 2 +- codex-rs/tui_app_server/src/app_server_session.rs | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 84c8120a98d..afbbda2caee 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3641,14 +3641,14 @@ pub struct Turn { #[ts(export_to = "v2/")] pub struct MemoryCitation { pub entries: Vec, - pub rollout_ids: Vec, + pub thread_ids: Vec, } impl From for MemoryCitation { fn from(value: CoreMemoryCitation) -> Self { Self { entries: value.entries.into_iter().map(Into::into).collect(), - rollout_ids: value.rollout_ids, + thread_ids: value.rollout_ids, } } } @@ -7544,7 +7544,7 @@ mod tests { line_end: 2, note: "summary".to_string(), }], - rollout_ids: vec!["rollout-1".to_string()], + thread_ids: vec!["rollout-1".to_string()], }), } ); diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c3e9db35ca3..67bc310da13 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -330,7 +330,7 @@ If this was the last subscriber, the server unloads the thread and emits `thread ### Example: Read a thread -Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want the rollout history loaded into `thread.turns`. The returned thread includes `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available. Assistant `ThreadItem::AgentMessage` items can also include `memoryCitation` metadata on final answers when the underlying response carried a parsed memory citation block. +Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want the rollout history loaded into `thread.turns`. The returned thread includes `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available. ```json { "method": "thread/read", "id": 22, "params": { "threadId": "thr_123" } } @@ -770,8 +770,6 @@ Event notifications are the server-initiated event stream for thread lifecycles, Thread realtime uses a separate thread-scoped notification surface. `thread/realtime/*` notifications are ephemeral transport events, not `ThreadItem`s, and are not returned by `thread/read`, `thread/resume`, or `thread/fork`. -Completed assistant `item/*` notifications reuse the same `ThreadItem::AgentMessage` shape as persisted history, including optional `memoryCitation` metadata for final answers. - ### Notification opt-out Clients can suppress specific notifications per connection by sending exact method names in `initialize.params.capabilities.optOutNotificationMethods`. diff --git a/codex-rs/tui_app_server/src/app/app_server_adapter.rs b/codex-rs/tui_app_server/src/app/app_server_adapter.rs index 71bcde35c49..bcf2a3f7318 100644 --- a/codex-rs/tui_app_server/src/app/app_server_adapter.rs +++ b/codex-rs/tui_app_server/src/app/app_server_adapter.rs @@ -450,7 +450,7 @@ fn thread_item_to_core(item: ThreadItem) -> Option { }, ) .collect(), - rollout_ids: citation.rollout_ids, + rollout_ids: citation.thread_ids, } }), })), diff --git a/codex-rs/tui_app_server/src/app_server_session.rs b/codex-rs/tui_app_server/src/app_server_session.rs index c43181bf810..8c00338fd4a 100644 --- a/codex-rs/tui_app_server/src/app_server_session.rs +++ b/codex-rs/tui_app_server/src/app_server_session.rs @@ -1059,7 +1059,7 @@ fn app_server_thread_item_to_core(item: codex_app_server_protocol::ThreadItem) - }, ) .collect(), - rollout_ids: citation.rollout_ids, + rollout_ids: citation.thread_ids, } }), })), From aa44af2b4b8e779caed248465389ed9ee607a700 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Tue, 17 Mar 2026 22:06:53 +0000 Subject: [PATCH 6/7] fix comments --- .../schema/json/ServerNotification.json | 4 ++-- .../json/codex_app_server_protocol.schemas.json | 4 ++-- .../json/codex_app_server_protocol.v2.schemas.json | 4 ++-- .../schema/json/v2/ItemCompletedNotification.json | 4 ++-- .../schema/json/v2/ItemStartedNotification.json | 4 ++-- .../schema/json/v2/ReviewStartResponse.json | 4 ++-- .../schema/json/v2/ThreadForkResponse.json | 4 ++-- .../schema/json/v2/ThreadListResponse.json | 4 ++-- .../json/v2/ThreadMetadataUpdateResponse.json | 4 ++-- .../schema/json/v2/ThreadReadResponse.json | 4 ++-- .../schema/json/v2/ThreadResumeResponse.json | 4 ++-- .../schema/json/v2/ThreadRollbackResponse.json | 4 ++-- .../schema/json/v2/ThreadStartResponse.json | 4 ++-- .../schema/json/v2/ThreadStartedNotification.json | 4 ++-- .../schema/json/v2/ThreadUnarchiveResponse.json | 4 ++-- .../schema/json/v2/TurnCompletedNotification.json | 4 ++-- .../schema/json/v2/TurnStartResponse.json | 4 ++-- .../schema/json/v2/TurnStartedNotification.json | 4 ++-- .../schema/typescript/v2/MemoryCitation.ts | 2 +- codex-rs/tui_app_server/src/app.rs | 2 ++ .../tui_app_server/src/app/app_server_adapter.rs | 14 +++++++++----- codex-rs/tui_app_server/src/chatwidget/tests.rs | 7 ++----- 22 files changed, 50 insertions(+), 47 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 5adbcf3bc03..8af79020342 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -1462,7 +1462,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -1471,7 +1471,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, 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 9cceef77e13..59e909bb36f 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 @@ -8682,7 +8682,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -8691,7 +8691,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, 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 dc63fa88e60..7b19330ab86 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 @@ -5470,7 +5470,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -5479,7 +5479,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 4bc6ba2ce80..f165850bf67 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -297,7 +297,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -306,7 +306,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index cbee3c60ae5..811e02c5a1c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -297,7 +297,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -306,7 +306,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 5bb05e2b1e5..aeb4db80ef9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -411,7 +411,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -420,7 +420,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index d0dece52526..04765cf484a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -496,7 +496,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -505,7 +505,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 3ed402c271e..9366304000c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -434,7 +434,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -443,7 +443,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index f2d492407f2..57dea225e25 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -434,7 +434,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -443,7 +443,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index dd105891db1..295938ba855 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -434,7 +434,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -443,7 +443,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 0f7371c067b..774c3cade36 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -496,7 +496,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -505,7 +505,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 9c1681c987b..518f560a278 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -434,7 +434,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -443,7 +443,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index e01803c994c..a6746e1eb18 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -496,7 +496,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -505,7 +505,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 6217a9cebee..a2307578d2d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -434,7 +434,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -443,7 +443,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 992e9b1d710..64c00271fb8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -434,7 +434,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -443,7 +443,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index a1d864ef03d..163d22b6426 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -411,7 +411,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -420,7 +420,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index bdaceed51bb..9264d98d29c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -411,7 +411,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -420,7 +420,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index c4deb836b80..5ed40f55f9b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -411,7 +411,7 @@ }, "type": "array" }, - "rolloutIds": { + "threadIds": { "items": { "type": "string" }, @@ -420,7 +420,7 @@ }, "required": [ "entries", - "rolloutIds" + "threadIds" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/MemoryCitation.ts b/codex-rs/app-server-protocol/schema/typescript/v2/MemoryCitation.ts index 4760e0bc892..7657e29f8bf 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/MemoryCitation.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/MemoryCitation.ts @@ -3,4 +3,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { MemoryCitationEntry } from "./MemoryCitationEntry"; -export type MemoryCitation = { entries: Array, rolloutIds: Array, }; +export type MemoryCitation = { entries: Array, threadIds: Array, }; diff --git a/codex-rs/tui_app_server/src/app.rs b/codex-rs/tui_app_server/src/app.rs index c08e74c3374..077a04e4d09 100644 --- a/codex-rs/tui_app_server/src/app.rs +++ b/codex-rs/tui_app_server/src/app.rs @@ -6770,6 +6770,7 @@ guardian_approval = true id: "assistant-1".to_string(), text: "restored response".to_string(), phase: None, + memory_citation: None, }, ], status: TurnStatus::Completed, @@ -6883,6 +6884,7 @@ guardian_approval = true id: "assistant-1".to_string(), text: "restored response".to_string(), phase: None, + memory_citation: None, }, ], status: TurnStatus::Completed, diff --git a/codex-rs/tui_app_server/src/app/app_server_adapter.rs b/codex-rs/tui_app_server/src/app/app_server_adapter.rs index 60bb19de83c..3fef2eda8f6 100644 --- a/codex-rs/tui_app_server/src/app/app_server_adapter.rs +++ b/codex-rs/tui_app_server/src/app/app_server_adapter.rs @@ -574,10 +574,10 @@ fn thread_item_to_core(item: &ThreadItem) -> Option { phase, memory_citation, } => Some(TurnItem::AgentMessage(AgentMessageItem { - id, - content: vec![AgentMessageContent::Text { text }], - phase, - memory_citation: memory_citation.map(|citation| { + id: id.clone(), + content: vec![AgentMessageContent::Text { text: text.clone() }], + phase: phase.clone(), + memory_citation: memory_citation.clone().map(|citation| { codex_protocol::memory_citation::MemoryCitation { entries: citation .entries @@ -595,7 +595,10 @@ fn thread_item_to_core(item: &ThreadItem) -> Option { } }), })), - ThreadItem::Plan { id, text } => Some(TurnItem::Plan(PlanItem { id, text })), + ThreadItem::Plan { id, text } => Some(TurnItem::Plan(PlanItem { + id: id.clone(), + text: text.clone(), + })), ThreadItem::Reasoning { id, summary, @@ -928,6 +931,7 @@ mod tests { id: "assistant-1".to_string(), text: "hi".to_string(), phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, }, ], status: TurnStatus::Completed, diff --git a/codex-rs/tui_app_server/src/chatwidget/tests.rs b/codex-rs/tui_app_server/src/chatwidget/tests.rs index f3efeb3a585..ceab56434d7 100644 --- a/codex-rs/tui_app_server/src/chatwidget/tests.rs +++ b/codex-rs/tui_app_server/src/chatwidget/tests.rs @@ -8417,11 +8417,8 @@ async fn permissions_selection_history_snapshot_full_access_to_default() { .approval_policy .set(AskForApproval::Never) .expect("set approval policy"); - chat.config - .permissions - .sandbox_policy - .set(SandboxPolicy::DangerFullAccess) - .expect("set sandbox policy"); + chat.config.permissions.sandbox_policy = + Constrained::allow_any(SandboxPolicy::DangerFullAccess); chat.open_permissions_popup(); let popup = render_bottom_popup(&chat, 120); From 89ef727cb704bd6be614ad256a5eb5378ab48963 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Wed, 18 Mar 2026 09:17:56 +0000 Subject: [PATCH 7/7] nit --- codex-rs/core/config.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 7d3ecdaa012..654e9039ac1 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2479,4 +2479,4 @@ }, "title": "ConfigToml", "type": "object" -} +} \ No newline at end of file