From 0b98172acaeb819ecb84b4d60c8462bf9c049b16 Mon Sep 17 00:00:00 2001 From: Ellen Blaine <67385198+ellenblaine@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:43:10 -0400 Subject: [PATCH] fix(prompt): make prompt timestamps timezone-explicit (#10209) Co-authored-by: Claude Opus 4.8 Co-authored-by: Douwe M Osinga --- crates/goose/src/agents/moim.rs | 18 +++++++++++++++++- crates/goose/src/agents/prompt_manager.rs | 15 +++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/goose/src/agents/moim.rs b/crates/goose/src/agents/moim.rs index c6ef1d99f..eb398dd99 100644 --- a/crates/goose/src/agents/moim.rs +++ b/crates/goose/src/agents/moim.rs @@ -142,7 +142,7 @@ fn compose_moim( max_turns: u32, extension_parts: Vec, ) -> String { - let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:00"); + let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:00 %:z"); let mut lines = vec![ open_tag(TURN_CONTEXT_TAG), tag(CURRENT_TIME_TAG, ×tamp.to_string()), @@ -437,5 +437,21 @@ mod tests { match: the detector's shape checks are what discriminate the injected block" ); } + + #[test] + fn current_time_is_timezone_aware() { + let block = moim(None, None, 0, 0, vec![]); + let time_line = block + .lines() + .find(|line| line.starts_with(&format!("<{CURRENT_TIME_TAG}>"))) + .expect("turn-context block should contain a current-time line"); + + let value = time_line + .trim_start_matches(&format!("<{CURRENT_TIME_TAG}>")) + .trim_end_matches(&format!("")); + + chrono::DateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S %:z") + .expect("current-time should include a numeric UTC offset"); + } } } diff --git a/crates/goose/src/agents/prompt_manager.rs b/crates/goose/src/agents/prompt_manager.rs index 810d7e673..5cb279191 100644 --- a/crates/goose/src/agents/prompt_manager.rs +++ b/crates/goose/src/agents/prompt_manager.rs @@ -206,7 +206,7 @@ impl PromptManager { system_prompt_extras: IndexMap::new(), // Use the fixed current date time so that prompt cache can be used. // Filtering to an hour to balance user time accuracy and multi session prompt cache hits. - current_date_timestamp: Utc::now().format("%Y-%m-%d %H:00").to_string(), + current_date_timestamp: Utc::now().format("%Y-%m-%d %H:00 %:z").to_string(), subdirectory_hint_tracker: SubdirectoryHintTracker::new(), } } @@ -216,7 +216,7 @@ impl PromptManager { PromptManager { system_prompt_override: None, system_prompt_extras: IndexMap::new(), - current_date_timestamp: dt.format("%Y-%m-%d %H:%M:%S").to_string(), + current_date_timestamp: dt.format("%Y-%m-%d %H:%M:%S %:z").to_string(), subdirectory_hint_tracker: SubdirectoryHintTracker::new(), } } @@ -300,6 +300,17 @@ mod tests { assert!(result.contains("with hidden text")); } + #[test] + fn test_current_date_time_includes_timezone() { + let mut manager = + PromptManager::with_timestamp(DateTime::::from_timestamp(0, 0).unwrap()); + manager.set_system_prompt_override("It is currently {{current_date_time}}".to_string()); + + let result = manager.builder().build(); + + assert_eq!(result, "It is currently 1970-01-01 00:00:00 +00:00"); + } + #[test] fn test_build_system_prompt_sanitizes_extras() { let mut manager = PromptManager::new();