Inject history notes hints into context windows (#40539)

## What changed

- Register the history-notes extension as a context contributor and fetch `thread_hint` from the configured history-notes backend.
- Add non-empty hints of at most 4,000 bytes to the context-window prompt, and omit hints when the request fails or the response is missing, empty, or oversized.
- Verify that app-server sessions use the native backend when `use_history_notes_extension` is enabled and retain the MCP bridge behavior when it is disabled.

## Testing

- Cover hint injection, request context, response validation, backend failures, and disabled-extension behavior in history-notes and app-server tests.

GitOrigin-RevId: 344a7bb79d962ea8b7c969f25b67a79423fc7fcc
This commit is contained in:
pmccrary-oai authored and copyberry committed 2026-08-25 04:05:38 +00:00
1 parent 3a469a297d
commit 2e4675919e
3 files changed
+249 -20

No files matched your search

@@ -12,10 +12,13 @@ use codex_config::types::AuthCredentialsStoreMode;
use core_test_support::load_default_config_for_test;
use core_test_support::responses;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use tempfile::TempDir;
use test_case::test_case;
use tokio::time::timeout;
use wiremock::Mock;
use wiremock::Request;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
@@ -25,8 +28,20 @@ use super::analytics::wait_for_matching_analytics_event;
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(30);
const THREAD_HINT: &str =
"Recent notes (up to 5, most-recent first):\n- /root/notes/latest.md (2 lines, 14 UTF-8 bytes)";
const BRIDGE_HINT: &str = "unstructured notes/thread_hint fixture result";
#[test_case(true, 200, THREAD_HINT; "native_hint")]
#[test_case(true, 200, ""; "no_notes")]
#[test_case(true, 503, THREAD_HINT; "native_failure_does_not_use_bridge")]
#[test_case(false, 200, THREAD_HINT; "bridge_hint")]
#[tokio::test]
async fn app_server_registers_history_and_notes_tools_for_token_budget_threads() -> Result<()> {
async fn app_server_uses_configured_notes_backend_for_context_window_hints(
use_history_notes_extension: bool,
hint_status: u16,
hint_text: &str,
) -> Result<()> {
let server = responses::start_mock_server().await;
let response_mock = responses::mount_sse_once(
&server,
@@ -37,6 +52,66 @@ async fn app_server_registers_history_and_notes_tools_for_token_budget_threads()
]),
)
.await;
// HTTP MCP stays on the app-server host, including with a remote executor.
Mock::given(method("POST"))
.and(path("/mcp"))
.respond_with(|request: &Request| {
let body: Value = request.body_json().expect("MCP JSON-RPC request");
let Some(id) = body.get("id") else {
return ResponseTemplate::new(202);
};
let result = match body["method"].as_str() {
Some("initialize") => json!({
"protocolVersion": body["params"]["protocolVersion"],
"capabilities": {"tools": {}},
"serverInfo": {"name": "notes-fixture", "version": "1.0.0"},
}),
Some("tools/list") => json!({
"tools": [{
"name": "thread_hint",
"inputSchema": {"type": "object", "properties": {}},
"annotations": {"readOnlyHint": true},
"_meta": {"ui": {"visibility": []}},
}],
}),
Some("tools/call") => {
assert_eq!(body["params"]["name"], "thread_hint");
let thread_id = body["params"]["_meta"]["threadId"]
.as_str()
.expect("threadId metadata");
json!({"content": [
{"type": "text", "text": format!("manual history hint for thread {thread_id}")},
{"type": "text", "text": BRIDGE_HINT},
]})
}
_ => {
return ResponseTemplate::new(200).set_body_json(json!({
"jsonrpc": "2.0",
"id": id,
"error": {"code": -32601, "message": "Method not found"},
}));
}
};
ResponseTemplate::new(200).set_body_json(json!({
"jsonrpc": "2.0", "id": id, "result": result,
}))
})
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/mcp"))
.respond_with(ResponseTemplate::new(405))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/backend-api/codex/alpha/notes/v2/thread_hint"))
.respond_with(
ResponseTemplate::new(hint_status).set_body_json(serde_json::json!({
"text": hint_text
})),
)
.mount(&server)
.await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri())
@@ -44,9 +119,10 @@ async fn app_server_registers_history_and_notes_tools_for_token_budget_threads()
.with_provider_name("OpenAI")
.with_provider_base_url(&format!("{}/backend-api/codex", server.uri()))
.with_provider_config("supports_websockets = false\nrequires_openai_auth = true")
.with_extra_config(
"[features.token_budget]\nenabled = true\nuse_history_notes_extension = true",
)
.with_extra_config(&format!(
"[features.token_budget]\nenabled = true\nuse_history_notes_extension = {use_history_notes_extension}\n\n[mcp_servers.notes]\nurl = \"{}/mcp\"\nstartup_timeout_sec = 10\n",
server.uri(),
))
.write(codex_home.path())?;
write_chatgpt_auth(
codex_home.path(),
@@ -77,22 +153,69 @@ async fn app_server_registers_history_and_notes_tools_for_token_budget_threads()
.await??;
let request = response_mock.single_request();
for (namespace, tool_name) in [
("history", "list_windows"),
("history", "list_items"),
("history", "read_item"),
("history", "search_contents"),
("notes", "list_files_by_prefix"),
("notes", "read_file"),
("notes", "search_contents"),
("notes", "append_to_file"),
("notes", "write_file"),
] {
assert!(
request.tool_by_name(namespace, tool_name).is_some(),
"app-server should expose {namespace}.{tool_name} to the model"
);
if use_history_notes_extension {
for (namespace, tool_name) in [
("history", "list_windows"),
("history", "list_items"),
("history", "read_item"),
("history", "search_contents"),
("notes", "list_files_by_prefix"),
("notes", "read_file"),
("notes", "search_contents"),
("notes", "append_to_file"),
("notes", "write_file"),
] {
assert!(
request.tool_by_name(namespace, tool_name).is_some(),
"app-server should expose {namespace}.{tool_name} to the model"
);
}
}
assert!(request.tool_by_name("notes", "thread_hint").is_none());
let input = request.input();
let bridge_hint_present = request
.message_input_texts("developer")
.iter()
.any(|text| text.contains(BRIDGE_HINT));
assert_eq!(bridge_hint_present, !use_history_notes_extension);
let requests = server.received_requests().await.expect("recorded requests");
let native_requests = requests
.iter()
.filter(|request| request.url.path() == "/backend-api/codex/alpha/notes/v2/thread_hint")
.count();
assert_eq!(native_requests, usize::from(use_history_notes_extension));
let bridge_calls = requests
.iter()
.filter(|request| request.url.path() == "/mcp")
.filter(|request| {
request
.body_json::<Value>()
.is_ok_and(|body| body["method"] == "tools/call")
})
.count();
assert_eq!(bridge_calls, usize::from(!use_history_notes_extension));
let developer_messages = request.message_input_texts("developer");
let context_window = developer_messages
.iter()
.find(|text| text.contains("<context_window>"))
.expect("context-window developer message");
let window_body = context_window
.split_once("<context_window>")
.expect("opening tag")
.1
.split_once("</context_window>")
.expect("closing tag")
.0;
assert_eq!(
window_body.contains(THREAD_HINT),
use_history_notes_extension && hint_status == 200 && !hint_text.is_empty(),
);
assert!(!input.iter().any(|item| {
item["type"] == "function_call"
&& item["namespace"] == "notes"
&& item["name"] == "thread_hint"
}));
Ok(())
}