diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 9f1c46d80..5039a0bfe 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -800,7 +800,7 @@ "description": "Stop filesystem watch notifications for a prior `fs/watch`.", "properties": { "watchId": { - "description": "Watch identifier returned by `fs/watch`.", + "description": "Watch identifier previously provided to `fs/watch`.", "type": "string" } }, @@ -819,10 +819,15 @@ } ], "description": "Absolute file or directory path to watch." + }, + "watchId": { + "description": "Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.", + "type": "string" } }, "required": [ - "path" + "path", + "watchId" ], "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index d7619354f..d550f523c 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -1013,7 +1013,7 @@ "type": "array" }, "watchId": { - "description": "Watch identifier returned by `fs/watch`.", + "description": "Watch identifier previously provided to `fs/watch`.", "type": "string" } }, 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 09f0daefd..da2169987 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 @@ -7511,7 +7511,7 @@ "type": "array" }, "watchId": { - "description": "Watch identifier returned by `fs/watch`.", + "description": "Watch identifier previously provided to `fs/watch`.", "type": "string" } }, @@ -7781,7 +7781,7 @@ "description": "Stop filesystem watch notifications for a prior `fs/watch`.", "properties": { "watchId": { - "description": "Watch identifier returned by `fs/watch`.", + "description": "Watch identifier previously provided to `fs/watch`.", "type": "string" } }, @@ -7808,25 +7808,6 @@ } ], "description": "Absolute file or directory path to watch." - } - }, - "required": [ - "path" - ], - "title": "FsWatchParams", - "type": "object" - }, - "FsWatchResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Created watch handle returned by `fs/watch`.", - "properties": { - "path": { - "allOf": [ - { - "$ref": "#/definitions/v2/AbsolutePathBuf" - } - ], - "description": "Canonicalized path associated with the watch." }, "watchId": { "description": "Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.", @@ -7837,6 +7818,25 @@ "path", "watchId" ], + "title": "FsWatchParams", + "type": "object" + }, + "FsWatchResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/watch`.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "Canonicalized path associated with the watch." + } + }, + "required": [ + "path" + ], "title": "FsWatchResponse", "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 620495f8b..a66c34e14 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 @@ -4179,7 +4179,7 @@ "type": "array" }, "watchId": { - "description": "Watch identifier returned by `fs/watch`.", + "description": "Watch identifier previously provided to `fs/watch`.", "type": "string" } }, @@ -4449,7 +4449,7 @@ "description": "Stop filesystem watch notifications for a prior `fs/watch`.", "properties": { "watchId": { - "description": "Watch identifier returned by `fs/watch`.", + "description": "Watch identifier previously provided to `fs/watch`.", "type": "string" } }, @@ -4476,25 +4476,6 @@ } ], "description": "Absolute file or directory path to watch." - } - }, - "required": [ - "path" - ], - "title": "FsWatchParams", - "type": "object" - }, - "FsWatchResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Created watch handle returned by `fs/watch`.", - "properties": { - "path": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Canonicalized path associated with the watch." }, "watchId": { "description": "Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.", @@ -4505,6 +4486,25 @@ "path", "watchId" ], + "title": "FsWatchParams", + "type": "object" + }, + "FsWatchResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/watch`.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Canonicalized path associated with the watch." + } + }, + "required": [ + "path" + ], "title": "FsWatchResponse", "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsChangedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/FsChangedNotification.json index ab26588ec..cfb9f4e5b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/FsChangedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/FsChangedNotification.json @@ -16,7 +16,7 @@ "type": "array" }, "watchId": { - "description": "Watch identifier returned by `fs/watch`.", + "description": "Watch identifier previously provided to `fs/watch`.", "type": "string" } }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsUnwatchParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsUnwatchParams.json index 4b988d97a..f46800e99 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/FsUnwatchParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/FsUnwatchParams.json @@ -3,7 +3,7 @@ "description": "Stop filesystem watch notifications for a prior `fs/watch`.", "properties": { "watchId": { - "description": "Watch identifier returned by `fs/watch`.", + "description": "Watch identifier previously provided to `fs/watch`.", "type": "string" } }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsWatchParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsWatchParams.json index cf80c7b10..29a1ceea1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/FsWatchParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/FsWatchParams.json @@ -15,10 +15,15 @@ } ], "description": "Absolute file or directory path to watch." + }, + "watchId": { + "description": "Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.", + "type": "string" } }, "required": [ - "path" + "path", + "watchId" ], "title": "FsWatchParams", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsWatchResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsWatchResponse.json index b516636a0..abc7d466b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/FsWatchResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/FsWatchResponse.json @@ -6,7 +6,7 @@ "type": "string" } }, - "description": "Created watch handle returned by `fs/watch`.", + "description": "Successful response for `fs/watch`.", "properties": { "path": { "allOf": [ @@ -15,15 +15,10 @@ } ], "description": "Canonicalized path associated with the watch." - }, - "watchId": { - "description": "Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.", - "type": "string" } }, "required": [ - "path", - "watchId" + "path" ], "title": "FsWatchResponse", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsChangedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsChangedNotification.ts index 2e9bd0d6e..fc0fa9cf6 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/FsChangedNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsChangedNotification.ts @@ -8,7 +8,7 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf"; */ export type FsChangedNotification = { /** - * Watch identifier returned by `fs/watch`. + * Watch identifier previously provided to `fs/watch`. */ watchId: string, /** diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsUnwatchParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsUnwatchParams.ts index b21befdb5..34b70a930 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/FsUnwatchParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsUnwatchParams.ts @@ -7,6 +7,6 @@ */ export type FsUnwatchParams = { /** - * Watch identifier returned by `fs/watch`. + * Watch identifier previously provided to `fs/watch`. */ watchId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsWatchParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsWatchParams.ts index d6d956b28..60755538c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/FsWatchParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsWatchParams.ts @@ -7,6 +7,10 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf"; * Start filesystem watch notifications for an absolute path. */ export type FsWatchParams = { +/** + * Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`. + */ +watchId: string, /** * Absolute file or directory path to watch. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsWatchResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsWatchResponse.ts index 192727282..b640306d8 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/FsWatchResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsWatchResponse.ts @@ -4,13 +4,9 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf"; /** - * Created watch handle returned by `fs/watch`. + * Successful response for `fs/watch`. */ export type FsWatchResponse = { -/** - * Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`. - */ -watchId: string, /** * Canonicalized path associated with the watch. */ diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 8fbff6789..3ac96fa0e 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1635,6 +1635,7 @@ mod tests { let request = ClientRequest::FsWatch { request_id: RequestId::Integer(10), params: v2::FsWatchParams { + watch_id: "watch-git".to_string(), path: absolute_path("tmp/repo/.git"), }, }; @@ -1643,6 +1644,7 @@ mod tests { "method": "fs/watch", "id": 10, "params": { + "watchId": "watch-git", "path": absolute_path_string("tmp/repo/.git") } }), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 296baa1fe..e0d22bfa2 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2349,17 +2349,17 @@ pub struct FsCopyResponse {} #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct FsWatchParams { + /// Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`. + pub watch_id: String, /// Absolute file or directory path to watch. pub path: AbsolutePathBuf, } -/// Created watch handle returned by `fs/watch`. +/// Successful response for `fs/watch`. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct FsWatchResponse { - /// Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`. - pub watch_id: String, /// Canonicalized path associated with the watch. pub path: AbsolutePathBuf, } @@ -2369,7 +2369,7 @@ pub struct FsWatchResponse { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct FsUnwatchParams { - /// Watch identifier returned by `fs/watch`. + /// Watch identifier previously provided to `fs/watch`. pub watch_id: String, } @@ -2384,7 +2384,7 @@ pub struct FsUnwatchResponse {} #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct FsChangedNotification { - /// Watch identifier returned by `fs/watch`. + /// Watch identifier previously provided to `fs/watch`. pub watch_id: String, /// File or directory paths associated with this event. pub changed_paths: Vec, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 6fedd73d9..3aff64776 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -169,7 +169,7 @@ Example with notification opt-out: - `fs/readDirectory` — list direct child entries for an absolute directory path; each entry contains `fileName`, `isDirectory`, and `isFile`, and `fileName` is just the child name, not a path. - `fs/remove` — remove an absolute file or directory tree; `recursive` and `force` default to `true`. - `fs/copy` — copy between absolute paths; directory copies require `recursive: true`. -- `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path; returns a `watchId` and canonicalized `path`. +- `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path and caller-provided `watchId`; returns the canonicalized `path`. - `fs/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`. - `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`. - `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. @@ -816,10 +816,10 @@ All filesystem paths in this section must be absolute. ```json { "method": "fs/watch", "id": 44, "params": { + "watchId": "0195ec6b-1d6f-7c2e-8c7a-56f2c4a8b9d1", "path": "/Users/me/project/.git/HEAD" } } { "id": 44, "result": { - "watchId": "0195ec6b-1d6f-7c2e-8c7a-56f2c4a8b9d1", "path": "/Users/me/project/.git/HEAD" } } { "method": "fs/changed", "params": { diff --git a/codex-rs/app-server/src/fs_watch.rs b/codex-rs/app-server/src/fs_watch.rs index c8214c2b8..3a5b22624 100644 --- a/codex-rs/app-server/src/fs_watch.rs +++ b/codex-rs/app-server/src/fs_watch.rs @@ -1,3 +1,4 @@ +use crate::fs_api::invalid_request; use crate::outgoing_message::ConnectionId; use crate::outgoing_message::OutgoingMessageSender; use codex_app_server_protocol::FsChangedNotification; @@ -16,6 +17,7 @@ use codex_core::file_watcher::WatchRegistration; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::collections::HashSet; +use std::collections::hash_map::Entry; use std::hash::Hash; use std::path::PathBuf; use std::sync::Arc; @@ -26,7 +28,6 @@ use tokio::sync::mpsc; use tokio::sync::oneshot; use tokio::time::Instant; use tracing::warn; -use uuid::Uuid; const FS_CHANGED_NOTIFICATION_DEBOUNCE: Duration = Duration::from_millis(200); @@ -120,7 +121,11 @@ impl FsWatchManager { connection_id: ConnectionId, params: FsWatchParams, ) -> Result { - let watch_id = Uuid::now_v7().to_string(); + let watch_id = params.watch_id; + let watch_key = WatchKey { + connection_id, + watch_id: watch_id.clone(), + }; let outgoing = self.outgoing.clone(); let (subscriber, rx) = self.file_watcher.add_subscriber(); let watch_root = params.path.to_path_buf().clone(); @@ -130,17 +135,20 @@ impl FsWatchManager { }]); let (terminate_tx, terminate_rx) = oneshot::channel(); - self.state.lock().await.entries.insert( - WatchKey { - connection_id, - watch_id: watch_id.clone(), - }, - WatchEntry { - terminate_tx, - _subscriber: subscriber, - _registration: registration, - }, - ); + match self.state.lock().await.entries.entry(watch_key) { + Entry::Occupied(_) => { + return Err(invalid_request(format!( + "watchId already exists: {watch_id}" + ))); + } + Entry::Vacant(entry) => { + entry.insert(WatchEntry { + terminate_tx, + _subscriber: subscriber, + _registration: registration, + }); + } + } let task_watch_id = watch_id.clone(); tokio::spawn(async move { @@ -175,10 +183,7 @@ impl FsWatchManager { } }); - Ok(FsWatchResponse { - watch_id, - path: params.path, - }) + Ok(FsWatchResponse { path: params.path }) } pub(crate) async fn unwatch( @@ -216,7 +221,6 @@ mod tests { use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::TempDir; - use uuid::Version; fn absolute_path(path: PathBuf) -> AbsolutePathBuf { assert!( @@ -237,28 +241,33 @@ mod tests { } #[tokio::test] - async fn watch_returns_a_v7_id_and_tracks_the_owner_scoped_entry() { + async fn watch_uses_client_id_and_tracks_the_owner_scoped_entry() { let temp_dir = TempDir::new().expect("temp dir"); let head_path = temp_dir.path().join("HEAD"); std::fs::write(&head_path, "ref: refs/heads/main\n").expect("write HEAD"); let manager = manager_with_noop_watcher(); let path = absolute_path(head_path); + let watch_id = "watch-head".to_string(); let response = manager - .watch(ConnectionId(1), FsWatchParams { path: path.clone() }) + .watch( + ConnectionId(1), + FsWatchParams { + watch_id: watch_id.clone(), + path: path.clone(), + }, + ) .await .expect("watch should succeed"); assert_eq!(response.path, path); - let watch_id = Uuid::parse_str(&response.watch_id).expect("watch id should be a UUID"); - assert_eq!(watch_id.get_version(), Some(Version::SortRand)); let state = manager.state.lock().await; assert_eq!( state.entries.keys().cloned().collect::>(), HashSet::from([WatchKey { connection_id: ConnectionId(1), - watch_id: response.watch_id, + watch_id, }]) ); } @@ -270,10 +279,11 @@ mod tests { std::fs::write(&head_path, "ref: refs/heads/main\n").expect("write HEAD"); let manager = manager_with_noop_watcher(); - let response = manager + manager .watch( ConnectionId(1), FsWatchParams { + watch_id: "watch-head".to_string(), path: absolute_path(head_path), }, ) @@ -281,14 +291,14 @@ mod tests { .expect("watch should succeed"); let watch_key = WatchKey { connection_id: ConnectionId(1), - watch_id: response.watch_id.clone(), + watch_id: "watch-head".to_string(), }; manager .unwatch( ConnectionId(2), FsUnwatchParams { - watch_id: response.watch_id.clone(), + watch_id: "watch-head".to_string(), }, ) .await @@ -299,7 +309,7 @@ mod tests { .unwatch( ConnectionId(1), FsUnwatchParams { - watch_id: response.watch_id, + watch_id: "watch-head".to_string(), }, ) .await @@ -307,6 +317,41 @@ mod tests { assert!(!manager.state.lock().await.entries.contains_key(&watch_key)); } + #[tokio::test] + async fn watch_rejects_duplicate_id_for_the_same_connection() { + let temp_dir = TempDir::new().expect("temp dir"); + let head_path = temp_dir.path().join("HEAD"); + let fetch_head_path = temp_dir.path().join("FETCH_HEAD"); + std::fs::write(&head_path, "ref: refs/heads/main\n").expect("write HEAD"); + std::fs::write(&fetch_head_path, "old-fetch\n").expect("write FETCH_HEAD"); + + let manager = manager_with_noop_watcher(); + manager + .watch( + ConnectionId(1), + FsWatchParams { + watch_id: "watch-head".to_string(), + path: absolute_path(head_path), + }, + ) + .await + .expect("first watch should succeed"); + + let error = manager + .watch( + ConnectionId(1), + FsWatchParams { + watch_id: "watch-head".to_string(), + path: absolute_path(fetch_head_path), + }, + ) + .await + .expect_err("duplicate watch should fail"); + + assert_eq!(error.message, "watchId already exists: watch-head"); + assert_eq!(manager.state.lock().await.entries.len(), 1); + } + #[tokio::test] async fn connection_closed_removes_only_that_connections_watches() { let temp_dir = TempDir::new().expect("temp dir"); @@ -318,28 +363,31 @@ mod tests { std::fs::write(&packed_refs_path, "refs\n").expect("write packed-refs"); let manager = manager_with_noop_watcher(); - let response_1 = manager + let response = manager .watch( ConnectionId(1), FsWatchParams { - path: absolute_path(head_path), + watch_id: "watch-head".to_string(), + path: absolute_path(head_path.clone()), }, ) .await .expect("first watch should succeed"); - let response_2 = manager + manager .watch( ConnectionId(1), FsWatchParams { + watch_id: "watch-fetch-head".to_string(), path: absolute_path(fetch_head_path), }, ) .await .expect("second watch should succeed"); - let response_3 = manager + manager .watch( ConnectionId(2), FsWatchParams { + watch_id: "watch-packed-refs".to_string(), path: absolute_path(packed_refs_path), }, ) @@ -359,9 +407,9 @@ mod tests { .collect::>(), HashSet::from([WatchKey { connection_id: ConnectionId(2), - watch_id: response_3.watch_id, + watch_id: "watch-packed-refs".to_string(), }]) ); - assert_ne!(response_1.watch_id, response_2.watch_id); + assert_eq!(response.path, absolute_path(head_path)); } } diff --git a/codex-rs/app-server/tests/suite/v2/fs.rs b/codex-rs/app-server/tests/suite/v2/fs.rs index e11390480..3fd5d62c8 100644 --- a/codex-rs/app-server/tests/suite/v2/fs.rs +++ b/codex-rs/app-server/tests/suite/v2/fs.rs @@ -21,8 +21,6 @@ use std::path::PathBuf; use tempfile::TempDir; use tokio::time::Duration; use tokio::time::timeout; -use uuid::Uuid; -use uuid::Version; #[cfg(unix)] use std::os::unix::fs::symlink; @@ -628,8 +626,10 @@ async fn fs_watch_directory_reports_changed_child_paths_and_unwatch_stops_notifi std::fs::write(&fetch_head, "old\n")?; let mut mcp = initialized_mcp(&codex_home).await?; + let watch_id = "watch-git-dir".to_string(); let watch_request_id = mcp .send_fs_watch_request(codex_app_server_protocol::FsWatchParams { + watch_id: watch_id.clone(), path: absolute_path(git_dir.clone()), }) .await?; @@ -641,8 +641,6 @@ async fn fs_watch_directory_reports_changed_child_paths_and_unwatch_stops_notifi .await??, )?; assert_eq!(watch_response.path, absolute_path(git_dir.clone())); - let watch_id = Uuid::parse_str(&watch_response.watch_id)?; - assert_eq!(watch_id.get_version(), Some(Version::SortRand)); std::fs::write(&fetch_head, "updated\n")?; @@ -650,7 +648,7 @@ async fn fs_watch_directory_reports_changed_child_paths_and_unwatch_stops_notifi // Keep validating notification shape when the backend does emit, but do not // fail the whole suite if no OS event arrives. if let Some(changed) = maybe_fs_changed_notification(&mut mcp).await? { - assert_eq!(changed.watch_id, watch_response.watch_id.clone()); + assert_eq!(changed.watch_id, watch_id.clone()); assert_eq!( changed.changed_paths, vec![absolute_path(fetch_head.clone())] @@ -665,9 +663,7 @@ async fn fs_watch_directory_reports_changed_child_paths_and_unwatch_stops_notifi {} let unwatch_request_id = mcp - .send_fs_unwatch_request(FsUnwatchParams { - watch_id: watch_response.watch_id, - }) + .send_fs_unwatch_request(FsUnwatchParams { watch_id }) .await?; timeout( DEFAULT_READ_TIMEOUT, @@ -698,8 +694,10 @@ async fn fs_watch_file_reports_atomic_replace_events() -> Result<()> { std::fs::write(&head_path, "ref: refs/heads/main\n")?; let mut mcp = initialized_mcp(&codex_home).await?; + let watch_id = "watch-head".to_string(); let watch_request_id = mcp .send_fs_watch_request(codex_app_server_protocol::FsWatchParams { + watch_id: watch_id.clone(), path: absolute_path(head_path.clone()), }) .await?; @@ -718,7 +716,7 @@ async fn fs_watch_file_reports_atomic_replace_events() -> Result<()> { assert_eq!( changed, FsChangedNotification { - watch_id: watch_response.watch_id, + watch_id, changed_paths: vec![absolute_path(head_path.clone())], } ); @@ -735,8 +733,10 @@ async fn fs_watch_allows_missing_file_targets() -> Result<()> { std::fs::create_dir_all(&git_dir)?; let mut mcp = initialized_mcp(&codex_home).await?; + let watch_id = "watch-fetch-head".to_string(); let watch_request_id = mcp .send_fs_watch_request(codex_app_server_protocol::FsWatchParams { + watch_id: watch_id.clone(), path: absolute_path(fetch_head.clone()), }) .await?; @@ -755,7 +755,7 @@ async fn fs_watch_allows_missing_file_targets() -> Result<()> { assert_eq!( changed, FsChangedNotification { - watch_id: watch_response.watch_id, + watch_id, changed_paths: vec![absolute_path(fetch_head.clone())], } ); @@ -770,7 +770,10 @@ async fn fs_watch_rejects_relative_paths() -> Result<()> { let mut mcp = initialized_mcp(&codex_home).await?; let watch_id = mcp - .send_raw_request("fs/watch", Some(json!({ "path": "relative-path" }))) + .send_raw_request( + "fs/watch", + Some(json!({ "watchId": "watch-relative", "path": "relative-path" })), + ) .await?; expect_error_message( &mut mcp,