mirror of
https://github.com/openai/codex.git
synced 2026-11-06 08:31:30 +00:00
Expose loaded thread environments in app-server responses (#42386)
## What changed - Add the experimental `Thread.environments` field with each selected environment's ID, working directory, and runtime workspace roots. - Populate the field from live thread state in start, resume, read, list, and unarchive responses. Use `null` for unloaded threads and an empty list when a loaded thread has no environment selection. - Document that the field reports selection rather than connection status and does not affect executor selection or resume behavior. ## Testing - Cover selection updates, unloaded and resumed threads, loaded pathless unarchives, and clients that subscribe after environment connection notifications. GitOrigin-RevId: a6daa06aff7089d7c5a1e4c7d03109b41f12fa3c
This commit is contained in:
1 parent
cff76fa96f
commit
2b554fd3f9
42 files changed
+681
-11
No files matched your search
@@ -242,6 +242,7 @@ fn sample_thread_with_metadata(
|
|||||||
parent_thread_id: Option<String>,
|
parent_thread_id: Option<String>,
|
||||||
) -> Thread {
|
) -> Thread {
|
||||||
Thread {
|
Thread {
|
||||||
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: format!("session-{thread_id}"),
|
session_id: format!("session-{thread_id}"),
|
||||||
|
|||||||
@@ -588,6 +588,7 @@ fn sample_thread_archive_request() -> ClientRequest {
|
|||||||
|
|
||||||
fn sample_thread(thread_id: &str) -> Thread {
|
fn sample_thread(thread_id: &str) -> Thread {
|
||||||
Thread {
|
Thread {
|
||||||
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: format!("session-{thread_id}"),
|
session_id: format!("session-{thread_id}"),
|
||||||
|
|||||||
@@ -4338,6 +4338,29 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
+23
@@ -20553,6 +20553,29 @@
|
|||||||
"title": "ThreadDeletedNotification",
|
"title": "ThreadDeletedNotification",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/v2/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/v2/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
+23
@@ -18267,6 +18267,29 @@
|
|||||||
"title": "ThreadDeletedNotification",
|
"title": "ThreadDeletedNotification",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -1485,6 +1485,29 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -1276,6 +1276,29 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
+23
@@ -1276,6 +1276,29 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -1276,6 +1276,29 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -1485,6 +1485,29 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -1276,6 +1276,29 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -1276,6 +1276,29 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -1485,6 +1485,29 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -1276,6 +1276,29 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -1276,6 +1276,29 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ThreadEnvironment": {
|
||||||
|
"description": "An environment selected by a loaded thread, independent of connection status.",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"environmentId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"runtimeWorkspaceRoots": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/LegacyAppPathString"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"environmentId",
|
||||||
|
"runtimeWorkspaceRoots"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ThreadExtra": {
|
"ThreadExtra": {
|
||||||
"description": "Extra app-server data for a thread.",
|
"description": "Extra app-server data for a thread.",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,9 @@
|
|||||||
|
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||||
|
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { LegacyAppPathString } from "../LegacyAppPathString";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An environment selected by a loaded thread, independent of connection status.
|
||||||
|
*/
|
||||||
|
export type ThreadEnvironment = { environmentId: string, cwd: LegacyAppPathString, runtimeWorkspaceRoots: Array<LegacyAppPathString>, };
|
||||||
@@ -473,6 +473,7 @@ export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse";
|
|||||||
export type { ThreadDeleteParams } from "./ThreadDeleteParams";
|
export type { ThreadDeleteParams } from "./ThreadDeleteParams";
|
||||||
export type { ThreadDeleteResponse } from "./ThreadDeleteResponse";
|
export type { ThreadDeleteResponse } from "./ThreadDeleteResponse";
|
||||||
export type { ThreadDeletedNotification } from "./ThreadDeletedNotification";
|
export type { ThreadDeletedNotification } from "./ThreadDeletedNotification";
|
||||||
|
export type { ThreadEnvironment } from "./ThreadEnvironment";
|
||||||
export type { ThreadExtra } from "./ThreadExtra";
|
export type { ThreadExtra } from "./ThreadExtra";
|
||||||
export type { ThreadForkParams } from "./ThreadForkParams";
|
export type { ThreadForkParams } from "./ThreadForkParams";
|
||||||
export type { ThreadForkResponse } from "./ThreadForkResponse";
|
export type { ThreadForkResponse } from "./ThreadForkResponse";
|
||||||
|
|||||||
@@ -3128,6 +3128,7 @@ mod tests {
|
|||||||
request_id: RequestId::Integer(7),
|
request_id: RequestId::Integer(7),
|
||||||
response: v2::ThreadStartResponse {
|
response: v2::ThreadStartResponse {
|
||||||
thread: v2::Thread {
|
thread: v2::Thread {
|
||||||
|
environments: None,
|
||||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(),
|
session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(),
|
||||||
@@ -3186,6 +3187,7 @@ mod tests {
|
|||||||
"response": {
|
"response": {
|
||||||
"thread": {
|
"thread": {
|
||||||
"id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
"id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
||||||
|
"environments": null,
|
||||||
"extra": null,
|
"extra": null,
|
||||||
"sessionId": "67e55044-10b1-426f-9247-bb680e5fe0c7",
|
"sessionId": "67e55044-10b1-426f-9247-bb680e5fe0c7",
|
||||||
"forkedFromId": null,
|
"forkedFromId": null,
|
||||||
|
|||||||
@@ -1,9 +1,36 @@
|
|||||||
use crate::JsonSchema;
|
use crate::JsonSchema;
|
||||||
use crate::TS;
|
use crate::TS;
|
||||||
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||||
|
use codex_utils_path_uri::LegacyAppPathString;
|
||||||
use codex_utils_path_uri::PathUri;
|
use codex_utils_path_uri::PathUri;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// An environment selected by a loaded thread, independent of connection status.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ThreadEnvironment {
|
||||||
|
pub environment_id: String,
|
||||||
|
pub cwd: LegacyAppPathString,
|
||||||
|
pub runtime_workspace_roots: Vec<LegacyAppPathString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&TurnEnvironmentSelection> for ThreadEnvironment {
|
||||||
|
fn from(selection: &TurnEnvironmentSelection) -> Self {
|
||||||
|
Self {
|
||||||
|
environment_id: selection.environment_id.clone(),
|
||||||
|
cwd: selection.cwd.clone().into(),
|
||||||
|
runtime_workspace_roots: selection
|
||||||
|
.workspace_roots
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
|
|||||||
@@ -272,6 +272,13 @@ fn thread_resume_params_accept_turns_page_bootstrap() {
|
|||||||
fn thread_resume_response_round_trips_initial_turns_page() {
|
fn thread_resume_response_round_trips_initial_turns_page() {
|
||||||
let response = ThreadResumeResponse {
|
let response = ThreadResumeResponse {
|
||||||
thread: Thread {
|
thread: Thread {
|
||||||
|
environments: Some(vec![ThreadEnvironment {
|
||||||
|
environment_id: "remote".to_string(),
|
||||||
|
cwd: LegacyAppPathString::from_string(r"C:\workspace"),
|
||||||
|
runtime_workspace_roots: vec![LegacyAppPathString::from_string(
|
||||||
|
r"C:\workspace\src",
|
||||||
|
)],
|
||||||
|
}]),
|
||||||
id: "thr_123".to_string(),
|
id: "thr_123".to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: "thr_123".to_string(),
|
session_id: "thr_123".to_string(),
|
||||||
@@ -328,6 +335,12 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let value = serde_json::to_value(&response).expect("serialize thread resume response");
|
let value = serde_json::to_value(&response).expect("serialize thread resume response");
|
||||||
|
assert_eq!(
|
||||||
|
value["thread"]["environments"],
|
||||||
|
json!([{
|
||||||
|
"environmentId": "remote", "cwd": r"C:\workspace", "runtimeWorkspaceRoots": [r"C:\workspace\src"]
|
||||||
|
}])
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value["thread"]["section"],
|
value["thread"]["section"],
|
||||||
json!({
|
json!({
|
||||||
@@ -345,11 +358,13 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
|||||||
legacy_thread_fields.remove("section");
|
legacy_thread_fields.remove("section");
|
||||||
legacy_thread_fields.remove("sectionEnteredAt");
|
legacy_thread_fields.remove("sectionEnteredAt");
|
||||||
legacy_thread_fields.remove("projectId");
|
legacy_thread_fields.remove("projectId");
|
||||||
|
legacy_thread_fields.remove("environments");
|
||||||
let legacy_thread =
|
let legacy_thread =
|
||||||
serde_json::from_value::<Thread>(legacy_thread).expect("deserialize legacy thread");
|
serde_json::from_value::<Thread>(legacy_thread).expect("deserialize legacy thread");
|
||||||
assert_eq!(legacy_thread.section, None);
|
assert_eq!(legacy_thread.section, None);
|
||||||
assert_eq!(legacy_thread.section_entered_at, None);
|
assert_eq!(legacy_thread.section_entered_at, None);
|
||||||
assert_eq!(legacy_thread.project_id, None);
|
assert_eq!(legacy_thread.project_id, None);
|
||||||
|
assert_eq!(legacy_thread.environments, None);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value.get("initialTurnsPage"),
|
value.get("initialTurnsPage"),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::CodexErrorInfo;
|
use super::CodexErrorInfo;
|
||||||
|
use super::ThreadEnvironment;
|
||||||
use super::ThreadItem;
|
use super::ThreadItem;
|
||||||
use super::ThreadStatus;
|
use super::ThreadStatus;
|
||||||
use super::TurnStatus;
|
use super::TurnStatus;
|
||||||
@@ -203,6 +204,12 @@ pub struct ThreadSectionAppearance {
|
|||||||
pub struct Thread {
|
pub struct Thread {
|
||||||
/// Identifier for this thread. Codex-generated thread IDs are UUIDv7.
|
/// Identifier for this thread. Codex-generated thread IDs are UUIDv7.
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
/// Current environments for a loaded thread, in priority order, primary first.
|
||||||
|
/// `null` means the thread is not loaded or the server does not expose its selection.
|
||||||
|
/// An empty list means no environments are selected. This does not report connection status.
|
||||||
|
#[experimental("thread.environments")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub environments: Option<Vec<ThreadEnvironment>>,
|
||||||
/// Optional implementation-specific thread data.
|
/// Optional implementation-specific thread data.
|
||||||
#[experimental("thread.extra")]
|
#[experimental("thread.extra")]
|
||||||
pub extra: Option<ThreadExtra>,
|
pub extra: Option<ThreadExtra>,
|
||||||
@@ -286,6 +293,8 @@ pub struct Thread {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct ThreadCompatibility {
|
struct ThreadCompatibility {
|
||||||
id: String,
|
id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
environments: Option<Vec<ThreadEnvironment>>,
|
||||||
extra: Option<ThreadExtra>,
|
extra: Option<ThreadExtra>,
|
||||||
session_id: String,
|
session_id: String,
|
||||||
forked_from_id: Option<String>,
|
forked_from_id: Option<String>,
|
||||||
@@ -328,6 +337,7 @@ impl<'de> Deserialize<'de> for Thread {
|
|||||||
let thread = ThreadCompatibility::deserialize(deserializer)?;
|
let thread = ThreadCompatibility::deserialize(deserializer)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: thread.id,
|
id: thread.id,
|
||||||
|
environments: thread.environments,
|
||||||
extra: thread.extra,
|
extra: thread.extra,
|
||||||
session_id: thread.session_id,
|
session_id: thread.session_id,
|
||||||
forked_from_id: thread.forked_from_id,
|
forked_from_id: thread.forked_from_id,
|
||||||
|
|||||||
@@ -370,6 +370,13 @@ subscribing to it, or dispatching queued work or goal continuations.
|
|||||||
|
|
||||||
Start a fresh thread when you need a new Codex conversation.
|
Start a fresh thread when you need a new Codex conversation.
|
||||||
|
|
||||||
|
Experimental `Thread.environments` returns a loaded thread's current selection as `{ environmentId, cwd, runtimeWorkspaceRoots }` entries.
|
||||||
|
The first entry is the primary environment; paths use that environment's native syntax.
|
||||||
|
An empty list means no environments are selected; `null` means the thread is not loaded or the server does not expose its selection.
|
||||||
|
Start and resume responses report the resulting live selection, and read, list, and unarchive responses include it for loaded threads, even if the client missed `thread/environment/connected`.
|
||||||
|
The field is not persisted and does not change executor selection or resume behavior.
|
||||||
|
Reading an unloaded thread leaves it unloaded and returns `null`; use `environment/status` to check connection status separately.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "method": "thread/start", "id": 10, "params": {
|
{ "method": "thread/start", "id": 10, "params": {
|
||||||
// Optionally set config settings. If not specified, will use the user's
|
// Optionally set config settings. If not specified, will use the user's
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::error_code::invalid_request;
|
|||||||
use crate::notification_media::without_notification_media;
|
use crate::notification_media::without_notification_media;
|
||||||
use crate::outgoing_message::ClientRequestResult;
|
use crate::outgoing_message::ClientRequestResult;
|
||||||
use crate::outgoing_message::ThreadScopedOutgoingMessageSender;
|
use crate::outgoing_message::ThreadScopedOutgoingMessageSender;
|
||||||
use crate::request_processors::apply_live_model_settings;
|
use crate::request_processors::apply_live_thread_settings;
|
||||||
use crate::request_processors::populate_thread_turns_from_history;
|
use crate::request_processors::populate_thread_turns_from_history;
|
||||||
use crate::request_processors::thread_from_stored_thread;
|
use crate::request_processors::thread_from_stored_thread;
|
||||||
use crate::request_processors::thread_settings_from_config_snapshot;
|
use crate::request_processors::thread_settings_from_config_snapshot;
|
||||||
@@ -1287,7 +1287,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
apply_live_model_settings(&mut response.thread, &config_snapshot);
|
apply_live_thread_settings(&mut response.thread, &config_snapshot);
|
||||||
outgoing.send_response(request_id, response).await;
|
outgoing.send_response(request_id, response).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -605,12 +605,19 @@ use crate::thread_state::ThreadStateManager;
|
|||||||
use token_usage_replay::restored_token_usage_turn_id;
|
use token_usage_replay::restored_token_usage_turn_id;
|
||||||
use token_usage_replay::send_thread_token_usage_update_to_connection;
|
use token_usage_replay::send_thread_token_usage_update_to_connection;
|
||||||
|
|
||||||
pub(crate) fn apply_live_model_settings(
|
pub(crate) fn apply_live_thread_settings(
|
||||||
thread: &mut Thread,
|
thread: &mut Thread,
|
||||||
config_snapshot: &ThreadConfigSnapshot,
|
config_snapshot: &ThreadConfigSnapshot,
|
||||||
) {
|
) {
|
||||||
thread.model = Some(config_snapshot.model.clone());
|
thread.model = Some(config_snapshot.model.clone());
|
||||||
thread.reasoning_effort = config_snapshot.reasoning_effort.clone();
|
thread.reasoning_effort = config_snapshot.reasoning_effort.clone();
|
||||||
|
thread.environments = Some(
|
||||||
|
config_snapshot
|
||||||
|
.environment_selections()
|
||||||
|
.iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_request_cwd(cwd: Option<PathBuf>) -> Result<Option<AbsolutePathBuf>, JSONRPCErrorError> {
|
fn resolve_request_cwd(cwd: Option<PathBuf>) -> Result<Option<AbsolutePathBuf>, JSONRPCErrorError> {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::apply_live_model_settings;
|
use super::apply_live_thread_settings;
|
||||||
use super::thread_input::can_accept_direct_input;
|
use super::thread_input::can_accept_direct_input;
|
||||||
use crate::thread_status::ThreadWatchManager;
|
use crate::thread_status::ThreadWatchManager;
|
||||||
use crate::thread_status::resolve_thread_status;
|
use crate::thread_status::resolve_thread_status;
|
||||||
@@ -44,7 +44,7 @@ pub(super) async fn enrich_loaded_threads<T>(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let config_snapshot = loaded_thread.config_snapshot().await;
|
let config_snapshot = loaded_thread.config_snapshot().await;
|
||||||
apply_live_model_settings(thread, &config_snapshot);
|
apply_live_thread_settings(thread, &config_snapshot);
|
||||||
if !matches!(
|
if !matches!(
|
||||||
&thread.source,
|
&thread.source,
|
||||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
|
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
|
||||||
|
|||||||
@@ -1994,7 +1994,7 @@ impl ThreadRequestProcessor {
|
|||||||
if let Ok(loaded_thread) = self.thread_manager.get_thread(thread_uuid).await {
|
if let Ok(loaded_thread) = self.thread_manager.get_thread(thread_uuid).await {
|
||||||
thread.session_id = loaded_thread.session_configured().session_id.to_string();
|
thread.session_id = loaded_thread.session_configured().session_id.to_string();
|
||||||
let config_snapshot = loaded_thread.config_snapshot().await;
|
let config_snapshot = loaded_thread.config_snapshot().await;
|
||||||
apply_live_model_settings(&mut thread, &config_snapshot);
|
apply_live_thread_settings(&mut thread, &config_snapshot);
|
||||||
}
|
}
|
||||||
self.attach_thread_name(thread_uuid, &mut thread).await;
|
self.attach_thread_name(thread_uuid, &mut thread).await;
|
||||||
thread.status = resolve_thread_status(
|
thread.status = resolve_thread_status(
|
||||||
@@ -2049,6 +2049,10 @@ impl ThreadRequestProcessor {
|
|||||||
let (mut thread, _) =
|
let (mut thread, _) =
|
||||||
thread_from_stored_thread(stored_thread, fallback_provider.as_str(), &self.config.cwd);
|
thread_from_stored_thread(stored_thread, fallback_provider.as_str(), &self.config.cwd);
|
||||||
|
|
||||||
|
if let Ok(loaded_thread) = self.thread_manager.get_thread(thread_id).await {
|
||||||
|
let config_snapshot = loaded_thread.config_snapshot().await;
|
||||||
|
apply_live_thread_settings(&mut thread, &config_snapshot);
|
||||||
|
}
|
||||||
thread.status = resolve_thread_status(
|
thread.status = resolve_thread_status(
|
||||||
self.thread_watch_manager
|
self.thread_watch_manager
|
||||||
.loaded_status_for_thread(&thread.id)
|
.loaded_status_for_thread(&thread.id)
|
||||||
@@ -2985,7 +2989,7 @@ impl ThreadRequestProcessor {
|
|||||||
} else {
|
} else {
|
||||||
fallback_thread
|
fallback_thread
|
||||||
};
|
};
|
||||||
apply_live_model_settings(&mut thread, &config_snapshot);
|
apply_live_thread_settings(&mut thread, &config_snapshot);
|
||||||
self.apply_thread_read_store_fields(thread_id, &mut thread, include_turns, loaded_thread)
|
self.apply_thread_read_store_fields(thread_id, &mut thread, include_turns, loaded_thread)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(thread)
|
Ok(thread)
|
||||||
@@ -4284,7 +4288,7 @@ impl ThreadRequestProcessor {
|
|||||||
);
|
);
|
||||||
thread_summary.session_id = existing_thread.session_configured().session_id.to_string();
|
thread_summary.session_id = existing_thread.session_configured().session_id.to_string();
|
||||||
thread_summary.thread_source = config_snapshot.thread_source.clone().map(Into::into);
|
thread_summary.thread_source = config_snapshot.thread_source.clone().map(Into::into);
|
||||||
apply_live_model_settings(&mut thread_summary, &config_snapshot);
|
apply_live_thread_settings(&mut thread_summary, &config_snapshot);
|
||||||
thread_summary.can_accept_direct_input = Some(can_accept_direct_input(
|
thread_summary.can_accept_direct_input = Some(can_accept_direct_input(
|
||||||
existing_thread.multi_agent_version(),
|
existing_thread.multi_agent_version(),
|
||||||
&config_snapshot.session_source,
|
&config_snapshot.session_source,
|
||||||
@@ -4649,7 +4653,7 @@ impl ThreadRequestProcessor {
|
|||||||
)),
|
)),
|
||||||
};
|
};
|
||||||
let mut thread = thread?;
|
let mut thread = thread?;
|
||||||
apply_live_model_settings(&mut thread, &config_snapshot);
|
apply_live_thread_settings(&mut thread, &config_snapshot);
|
||||||
thread.can_accept_direct_input = Some(can_accept_direct_input);
|
thread.can_accept_direct_input = Some(can_accept_direct_input);
|
||||||
thread.id = thread_id.to_string();
|
thread.id = thread_id.to_string();
|
||||||
thread.session_id = session_id;
|
thread.session_id = session_id;
|
||||||
@@ -5147,7 +5151,7 @@ impl ThreadRequestProcessor {
|
|||||||
));
|
));
|
||||||
thread.session_id = session_configured.session_id.to_string();
|
thread.session_id = session_configured.session_id.to_string();
|
||||||
thread.thread_source = config_snapshot.thread_source.clone().map(Into::into);
|
thread.thread_source = config_snapshot.thread_source.clone().map(Into::into);
|
||||||
apply_live_model_settings(&mut thread, &config_snapshot);
|
apply_live_thread_settings(&mut thread, &config_snapshot);
|
||||||
if thread.path.is_none() {
|
if thread.path.is_none() {
|
||||||
thread.project_id = inherited_project_id.clone();
|
thread.project_id = inherited_project_id.clone();
|
||||||
}
|
}
|
||||||
@@ -5908,6 +5912,7 @@ pub(crate) fn thread_from_stored_thread(
|
|||||||
let thread_id = thread.thread_id.to_string();
|
let thread_id = thread.thread_id.to_string();
|
||||||
let thread = Thread {
|
let thread = Thread {
|
||||||
id: thread_id.clone(),
|
id: thread_id.clone(),
|
||||||
|
environments: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: thread_id,
|
session_id: thread_id,
|
||||||
forked_from_id: thread.forked_from_id.map(|id| id.to_string()),
|
forked_from_id: thread.forked_from_id.map(|id| id.to_string()),
|
||||||
@@ -6096,6 +6101,13 @@ fn build_thread_from_snapshot(
|
|||||||
let now = time::OffsetDateTime::now_utc().unix_timestamp();
|
let now = time::OffsetDateTime::now_utc().unix_timestamp();
|
||||||
Thread {
|
Thread {
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
|
environments: Some(
|
||||||
|
config_snapshot
|
||||||
|
.environment_selections()
|
||||||
|
.iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id,
|
session_id,
|
||||||
forked_from_id: None,
|
forked_from_id: None,
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ mod tests {
|
|||||||
|
|
||||||
fn test_thread(items: Vec<ThreadItem>) -> Thread {
|
fn test_thread(items: Vec<ThreadItem>) -> Thread {
|
||||||
Thread {
|
Thread {
|
||||||
|
environments: None,
|
||||||
id: "thread-1".to_string(),
|
id: "thread-1".to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: "session-1".to_string(),
|
session_id: "session-1".to_string(),
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ pub(crate) fn summary_to_thread(
|
|||||||
|
|
||||||
let thread_id = conversation_id.to_string();
|
let thread_id = conversation_id.to_string();
|
||||||
Thread {
|
Thread {
|
||||||
|
environments: None,
|
||||||
id: thread_id.clone(),
|
id: thread_id.clone(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: thread_id,
|
session_id: thread_id,
|
||||||
|
|||||||
@@ -1469,7 +1469,7 @@ impl TurnRequestProcessor {
|
|||||||
|
|
||||||
if let Some(mut thread) = stored_thread {
|
if let Some(mut thread) = stored_thread {
|
||||||
let config_snapshot = review_thread.config_snapshot().await;
|
let config_snapshot = review_thread.config_snapshot().await;
|
||||||
apply_live_model_settings(&mut thread, &config_snapshot);
|
apply_live_thread_settings(&mut thread, &config_snapshot);
|
||||||
thread.session_id = review_thread.session_configured().session_id.to_string();
|
thread.session_id = review_thread.session_configured().session_id.to_string();
|
||||||
self.thread_watch_manager
|
self.thread_watch_manager
|
||||||
.upsert_thread_silently(&thread.id)
|
.upsert_thread_silently(&thread.id)
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ mod skills_list;
|
|||||||
mod sleep;
|
mod sleep;
|
||||||
mod thread_archive;
|
mod thread_archive;
|
||||||
mod thread_delete;
|
mod thread_delete;
|
||||||
|
mod thread_environments;
|
||||||
mod thread_fork;
|
mod thread_fork;
|
||||||
mod thread_inject_items;
|
mod thread_inject_items;
|
||||||
mod thread_list;
|
mod thread_list;
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use app_test_support::MockResponsesConfig;
|
||||||
|
use app_test_support::TestAppServer;
|
||||||
|
use app_test_support::create_mock_responses_server_repeating_assistant;
|
||||||
|
use codex_app_server_protocol::ThreadEnvironment;
|
||||||
|
use codex_app_server_protocol::ThreadListParams;
|
||||||
|
use codex_app_server_protocol::ThreadListResponse;
|
||||||
|
use codex_app_server_protocol::ThreadReadParams;
|
||||||
|
use codex_app_server_protocol::ThreadReadResponse;
|
||||||
|
use codex_app_server_protocol::ThreadResumeParams;
|
||||||
|
use codex_app_server_protocol::ThreadResumeResponse;
|
||||||
|
use codex_app_server_protocol::ThreadStartParams;
|
||||||
|
use codex_app_server_protocol::ThreadStartResponse;
|
||||||
|
use codex_app_server_protocol::TurnStartParams;
|
||||||
|
use codex_app_server_protocol::TurnStatus;
|
||||||
|
use codex_app_server_protocol::UserInput;
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use tokio::time::timeout;
|
||||||
|
|
||||||
|
use super::connection_handling_websocket::DEFAULT_READ_TIMEOUT;
|
||||||
|
use super::connection_handling_websocket::connect_websocket;
|
||||||
|
use super::connection_handling_websocket::create_config_toml;
|
||||||
|
use super::connection_handling_websocket::read_notification_for_method;
|
||||||
|
use super::connection_handling_websocket::read_response_for_id;
|
||||||
|
use super::connection_handling_websocket::send_request;
|
||||||
|
use super::connection_handling_websocket::spawn_websocket_server;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_environments_follow_the_loaded_thread_selection() -> Result<()> {
|
||||||
|
let responses = create_mock_responses_server_repeating_assistant("Done").await;
|
||||||
|
let home = TempDir::new()?;
|
||||||
|
MockResponsesConfig::new(&responses.uri()).write(home.path())?;
|
||||||
|
let mut first = TestAppServer::builder()
|
||||||
|
.with_codex_home(home.path())
|
||||||
|
.build_initialized()
|
||||||
|
.await?;
|
||||||
|
let selection = first.auto_env_params()?;
|
||||||
|
let expected = Some(vec![ThreadEnvironment {
|
||||||
|
environment_id: selection.environment_id,
|
||||||
|
cwd: selection.cwd.clone(),
|
||||||
|
runtime_workspace_roots: vec![selection.cwd],
|
||||||
|
}]);
|
||||||
|
let request = first
|
||||||
|
.send_thread_start_request_with_auto_env(ThreadStartParams::default())
|
||||||
|
.await?;
|
||||||
|
let started: ThreadStartResponse =
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, first.read_response(request)).await??;
|
||||||
|
assert_eq!(started.thread.environments, expected);
|
||||||
|
let thread_id = started.thread.id;
|
||||||
|
let completed = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
first.start_turn_and_wait_for_completion(TurnStartParams {
|
||||||
|
thread_id: thread_id.clone(),
|
||||||
|
input: vec![UserInput::Text {
|
||||||
|
text: "hello".into(),
|
||||||
|
text_elements: vec![],
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
assert_eq!(completed.turn.status, TurnStatus::Completed);
|
||||||
|
assert_read_and_list_environments(&mut first, &thread_id, &expected).await?;
|
||||||
|
|
||||||
|
// Report the thread's explicit selection rather than the server-wide defaults.
|
||||||
|
let completed = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
first.start_turn_and_wait_for_completion(TurnStartParams {
|
||||||
|
thread_id: thread_id.clone(),
|
||||||
|
input: vec![UserInput::Text {
|
||||||
|
text: "hello again".into(),
|
||||||
|
text_elements: vec![],
|
||||||
|
}],
|
||||||
|
environments: Some(vec![]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
assert_eq!(completed.turn.status, TurnStatus::Completed);
|
||||||
|
assert_read_and_list_environments(&mut first, &thread_id, &Some(vec![])).await?;
|
||||||
|
let request = first
|
||||||
|
.send_thread_resume_request(ThreadResumeParams {
|
||||||
|
thread_id: thread_id.clone(),
|
||||||
|
exclude_turns: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let resumed: ThreadResumeResponse =
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, first.read_response(request)).await??;
|
||||||
|
assert_eq!(resumed.thread.environments, Some(vec![]));
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, first.shutdown_gracefully()).await??;
|
||||||
|
|
||||||
|
// Unloaded reads have no live selection. Resume reports its newly selected defaults.
|
||||||
|
let mut second = TestAppServer::builder()
|
||||||
|
.with_codex_home(home.path())
|
||||||
|
.build_initialized()
|
||||||
|
.await?;
|
||||||
|
assert_read_and_list_environments(&mut second, &thread_id, &None).await?;
|
||||||
|
let request = second
|
||||||
|
.send_thread_resume_request(ThreadResumeParams {
|
||||||
|
thread_id: thread_id.clone(),
|
||||||
|
exclude_turns: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let resumed: ThreadResumeResponse =
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, second.read_response(request)).await??;
|
||||||
|
let expected = Some(vec![ThreadEnvironment {
|
||||||
|
environment_id: second.auto_env_params()?.environment_id,
|
||||||
|
cwd: resumed.cwd.clone().into(),
|
||||||
|
runtime_workspace_roots: resumed
|
||||||
|
.runtime_workspace_roots
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect(),
|
||||||
|
}]);
|
||||||
|
assert_eq!(resumed.thread.environments, expected);
|
||||||
|
assert_read_and_list_environments(&mut second, &thread_id, &expected).await?;
|
||||||
|
let request = second
|
||||||
|
.send_thread_resume_request(ThreadResumeParams {
|
||||||
|
thread_id,
|
||||||
|
exclude_turns: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let resumed: ThreadResumeResponse =
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, second.read_response(request)).await??;
|
||||||
|
assert_eq!(resumed.thread.environments, expected);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn assert_read_and_list_environments(
|
||||||
|
server: &mut TestAppServer,
|
||||||
|
thread_id: &str,
|
||||||
|
expected: &Option<Vec<ThreadEnvironment>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let request = server
|
||||||
|
.send_thread_read_request(ThreadReadParams {
|
||||||
|
thread_id: thread_id.to_owned(),
|
||||||
|
include_turns: false,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let read: ThreadReadResponse =
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, server.read_response(request)).await??;
|
||||||
|
assert_eq!(&read.thread.environments, expected);
|
||||||
|
let request = server
|
||||||
|
.send_thread_list_request(serde_json::from_value::<ThreadListParams>(json!({}))?)
|
||||||
|
.await?;
|
||||||
|
let listed: ThreadListResponse =
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, server.read_response(request)).await??;
|
||||||
|
assert_eq!(
|
||||||
|
&listed
|
||||||
|
.data
|
||||||
|
.iter()
|
||||||
|
.find(|thread| thread.id == thread_id)
|
||||||
|
.expect("listed thread")
|
||||||
|
.environments,
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_environments_are_returned_to_a_late_subscriber() -> Result<()> {
|
||||||
|
let responses = create_mock_responses_server_repeating_assistant("Done").await;
|
||||||
|
let home = TempDir::new()?;
|
||||||
|
create_config_toml(home.path(), &responses.uri(), "never")?;
|
||||||
|
let workspace = TempDir::new()?;
|
||||||
|
let cwd = AbsolutePathBuf::from_absolute_path(workspace.path().canonicalize()?)?;
|
||||||
|
let expected =
|
||||||
|
json!([{ "environmentId": "local", "cwd": cwd, "runtimeWorkspaceRoots": [cwd] }]);
|
||||||
|
let (mut process, address) = spawn_websocket_server(home.path()).await?;
|
||||||
|
let mut first = connect_websocket(address).await?;
|
||||||
|
let initialize = json!({
|
||||||
|
"clientInfo": { "name": "environment-test", "version": "1" },
|
||||||
|
"capabilities": { "experimentalApi": true }
|
||||||
|
});
|
||||||
|
send_request(
|
||||||
|
&mut first,
|
||||||
|
"initialize",
|
||||||
|
/*id*/ 1,
|
||||||
|
Some(initialize.clone()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
read_response_for_id(&mut first, /*id*/ 1).await?;
|
||||||
|
send_request(
|
||||||
|
&mut first,
|
||||||
|
"thread/start",
|
||||||
|
/*id*/ 2,
|
||||||
|
Some(json!({
|
||||||
|
"environments": [{ "environmentId": "local", "cwd": cwd }]
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let started = read_response_for_id(&mut first, /*id*/ 2).await?;
|
||||||
|
assert_eq!(started.result["thread"]["environments"], expected);
|
||||||
|
let thread_id = started.result["thread"]["id"].as_str().unwrap();
|
||||||
|
send_request(
|
||||||
|
&mut first,
|
||||||
|
"turn/start",
|
||||||
|
/*id*/ 3,
|
||||||
|
Some(json!({
|
||||||
|
"threadId": thread_id, "input": [{ "type": "text", "text": "hello" }]
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
read_response_for_id(&mut first, /*id*/ 3).await?;
|
||||||
|
read_notification_for_method(&mut first, "turn/completed").await?;
|
||||||
|
|
||||||
|
let mut second = connect_websocket(address).await?;
|
||||||
|
send_request(&mut second, "initialize", /*id*/ 1, Some(initialize)).await?;
|
||||||
|
read_response_for_id(&mut second, /*id*/ 1).await?;
|
||||||
|
for (id, method, params) in [
|
||||||
|
(2, "thread/read", json!({ "threadId": thread_id })),
|
||||||
|
(
|
||||||
|
3,
|
||||||
|
"thread/resume",
|
||||||
|
json!({ "threadId": thread_id, "excludeTurns": true }),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
send_request(&mut second, method, id, Some(params)).await?;
|
||||||
|
let response = read_response_for_id(&mut second, id).await?;
|
||||||
|
assert_eq!(response.result["thread"]["environments"], expected);
|
||||||
|
}
|
||||||
|
process.kill().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -332,6 +332,37 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
|
|||||||
assert_eq!(thread.path, None);
|
assert_eq!(thread.path, None);
|
||||||
assert_eq!(thread.forked_from_id, Some(parent_thread_id.to_string()));
|
assert_eq!(thread.forked_from_id, Some(parent_thread_id.to_string()));
|
||||||
assert_eq!(thread.name, Some("named pathless thread".to_string()));
|
assert_eq!(thread.name, Some("named pathless thread".to_string()));
|
||||||
|
assert_eq!(thread.environments, None);
|
||||||
|
|
||||||
|
// Pathless stores can return an unarchived thread while it is still loaded.
|
||||||
|
for (request_id, environments) in [(2, None), (4, Some(vec![]))] {
|
||||||
|
let result = client
|
||||||
|
.request(ClientRequest::ThreadStart {
|
||||||
|
request_id: RequestId::Integer(request_id),
|
||||||
|
params: ThreadStartParams {
|
||||||
|
model: Some("mock-model".to_string()),
|
||||||
|
environments,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.expect("thread/start should succeed");
|
||||||
|
let ThreadStartResponse {
|
||||||
|
thread: started, ..
|
||||||
|
} = serde_json::from_value(result)?;
|
||||||
|
assert!(started.environments.is_some());
|
||||||
|
let result = client
|
||||||
|
.request(ClientRequest::ThreadUnarchive {
|
||||||
|
request_id: RequestId::Integer(request_id + 1),
|
||||||
|
params: ThreadUnarchiveParams {
|
||||||
|
thread_id: started.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.expect("thread/unarchive should succeed for a loaded pathless thread");
|
||||||
|
let ThreadUnarchiveResponse { thread } = serde_json::from_value(result)?;
|
||||||
|
assert_eq!(thread.environments, started.environments);
|
||||||
|
}
|
||||||
|
|
||||||
client.shutdown().await?;
|
client.shutdown().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -351,6 +351,7 @@ async fn resume_lookup_model_providers_filters_only_last_lookup() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn turn_items_for_thread_returns_matching_turn_items() {
|
fn turn_items_for_thread_returns_matching_turn_items() {
|
||||||
let thread = AppServerThread {
|
let thread = AppServerThread {
|
||||||
|
environments: None,
|
||||||
id: "thread-1".to_string(),
|
id: "thread-1".to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: "thread-1".to_string(),
|
session_id: "thread-1".to_string(),
|
||||||
@@ -841,6 +842,7 @@ async fn session_configured_from_thread_response_preserves_parent_thread_id() {
|
|||||||
fn sample_thread_start_response() -> ThreadStartResponse {
|
fn sample_thread_start_response() -> ThreadStartResponse {
|
||||||
ThreadStartResponse {
|
ThreadStartResponse {
|
||||||
thread: codex_app_server_protocol::Thread {
|
thread: codex_app_server_protocol::Thread {
|
||||||
|
environments: None,
|
||||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(),
|
session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ fn overview_thread(
|
|||||||
status: ThreadStatus,
|
status: ThreadStatus,
|
||||||
) -> Thread {
|
) -> Thread {
|
||||||
Thread {
|
Thread {
|
||||||
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
project_id: None,
|
project_id: None,
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ mod tests {
|
|||||||
|
|
||||||
fn test_thread(thread_id: ThreadId, source: SessionSource) -> Thread {
|
fn test_thread(thread_id: ThreadId, source: SessionSource) -> Thread {
|
||||||
Thread {
|
Thread {
|
||||||
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: thread_id.to_string(),
|
session_id: thread_id.to_string(),
|
||||||
|
|||||||
@@ -4039,6 +4039,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re
|
|||||||
agent_thread_id,
|
agent_thread_id,
|
||||||
ServerNotification::ThreadStarted(ThreadStartedNotification {
|
ServerNotification::ThreadStarted(ThreadStartedNotification {
|
||||||
thread: Thread {
|
thread: Thread {
|
||||||
|
environments: None,
|
||||||
id: agent_thread_id.to_string(),
|
id: agent_thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: agent_thread_id.to_string(),
|
session_id: agent_thread_id.to_string(),
|
||||||
@@ -4140,6 +4141,7 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_
|
|||||||
agent_thread_id,
|
agent_thread_id,
|
||||||
ServerNotification::ThreadStarted(ThreadStartedNotification {
|
ServerNotification::ThreadStarted(ThreadStartedNotification {
|
||||||
thread: Thread {
|
thread: Thread {
|
||||||
|
environments: None,
|
||||||
id: agent_thread_id.to_string(),
|
id: agent_thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: agent_thread_id.to_string(),
|
session_id: agent_thread_id.to_string(),
|
||||||
@@ -4208,6 +4210,7 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() {
|
|||||||
app.primary_session_configured = Some(primary_session);
|
app.primary_session_configured = Some(primary_session);
|
||||||
|
|
||||||
let thread = Thread {
|
let thread = Thread {
|
||||||
|
environments: None,
|
||||||
id: read_thread_id.to_string(),
|
id: read_thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: read_thread_id.to_string(),
|
session_id: read_thread_id.to_string(),
|
||||||
|
|||||||
@@ -933,6 +933,7 @@ async fn known_thread_started_preserves_session_without_reading_unmaterialized_r
|
|||||||
);
|
);
|
||||||
let notification = ThreadStartedNotification {
|
let notification = ThreadStartedNotification {
|
||||||
thread: Thread {
|
thread: Thread {
|
||||||
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: thread_id.to_string(),
|
session_id: thread_id.to_string(),
|
||||||
|
|||||||
@@ -410,6 +410,7 @@ mod tests {
|
|||||||
..test_thread_session(primary_thread_id, test_path_buf("/tmp/primary"))
|
..test_thread_session(primary_thread_id, test_path_buf("/tmp/primary"))
|
||||||
};
|
};
|
||||||
let read_thread = Thread {
|
let read_thread = Thread {
|
||||||
|
environments: None,
|
||||||
id: read_thread_id.to_string(),
|
id: read_thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: read_thread_id.to_string(),
|
session_id: read_thread_id.to_string(),
|
||||||
|
|||||||
@@ -3686,6 +3686,7 @@ mod tests {
|
|||||||
let read_only_profile = PermissionProfile::read_only();
|
let read_only_profile = PermissionProfile::read_only();
|
||||||
let response = ThreadResumeResponse {
|
let response = ThreadResumeResponse {
|
||||||
thread: codex_app_server_protocol::Thread {
|
thread: codex_app_server_protocol::Thread {
|
||||||
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: ThreadId::new().to_string(),
|
session_id: ThreadId::new().to_string(),
|
||||||
|
|||||||
@@ -6266,6 +6266,7 @@ session_picker_view = "dense"
|
|||||||
fn app_server_row_keeps_pathless_threads() {
|
fn app_server_row_keeps_pathless_threads() {
|
||||||
let thread_id = ThreadId::new();
|
let thread_id = ThreadId::new();
|
||||||
let thread = Thread {
|
let thread = Thread {
|
||||||
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: thread_id.to_string(),
|
session_id: thread_id.to_string(),
|
||||||
@@ -6310,6 +6311,7 @@ session_picker_view = "dense"
|
|||||||
|
|
||||||
let thread_id = ThreadId::new();
|
let thread_id = ThreadId::new();
|
||||||
let thread = Thread {
|
let thread = Thread {
|
||||||
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: thread_id.to_string(),
|
session_id: thread_id.to_string(),
|
||||||
@@ -6394,6 +6396,7 @@ session_picker_view = "dense"
|
|||||||
|
|
||||||
let thread_id = ThreadId::new();
|
let thread_id = ThreadId::new();
|
||||||
let thread = Thread {
|
let thread = Thread {
|
||||||
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: thread_id.to_string(),
|
session_id: thread_id.to_string(),
|
||||||
@@ -6469,6 +6472,7 @@ session_picker_view = "dense"
|
|||||||
|
|
||||||
let thread_id = ThreadId::new();
|
let thread_id = ThreadId::new();
|
||||||
let thread = Thread {
|
let thread = Thread {
|
||||||
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
session_id: thread_id.to_string(),
|
session_id: thread_id.to_string(),
|
||||||
|
|||||||
Reference in new issue
Block a user