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

@@ -4,11 +4,14 @@ use codex_config::LoaderOverrides;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::TokenBudgetConfig;
use codex_extension_api::ContentItemKind;
use codex_extension_api::ConversationHistory;
use codex_extension_api::ExtensionData;
use codex_extension_api::ExtensionRegistry;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::NoopTurnItemEmitter;
use codex_extension_api::PromptFragment;
use codex_extension_api::PromptSlot;
use codex_extension_api::ThreadStartInput;
use codex_extension_api::ToolCall;
use codex_extension_api::ToolCallSource;
@@ -40,6 +43,7 @@ use wiremock::matchers::method;
use wiremock::matchers::path;
type TestResult = Result<(), Box<dyn std::error::Error>>;
const THREAD_HINT: &str = "Recent notes (up to 5, most-recent first):\n- /root/worker/notes/latest.md (2 lines, 14 UTF-8 bytes)";
#[tokio::test]
async fn installed_extension_exposes_and_invokes_history_notes_tools() -> TestResult {
@@ -52,6 +56,12 @@ async fn installed_extension_exposes_and_invokes_history_notes_tools() -> TestRe
})))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/backend-api/codex/alpha/notes/v2/thread_hint"))
.and(header("x-openai-actor-authorization", "actor-biscuit"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"text": THREAD_HINT})))
.mount(&server)
.await;
let codex_home = TempDir::new()?;
let mut config = ConfigBuilder::default()
.loader_overrides(LoaderOverrides::without_managed_config_for_tests())
@@ -142,8 +152,20 @@ async fn installed_extension_exposes_and_invokes_history_notes_tools() -> TestRe
)
);
let hints = registry.context_contributors()[0]
.contribute_thread_context(&session_store, &thread_store)
.await;
assert_eq!(
hints,
vec![PromptFragment::new(
PromptSlot::ContextWindow,
THREAD_HINT,
ContentItemKind("notes.thread_hint".to_string()),
)]
);
let requests = server.received_requests().await.expect("recorded requests");
assert_eq!(requests.len(), 1);
assert_eq!(requests.len(), 2);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&requests[0].body)?,
json!({
@@ -155,12 +177,47 @@ async fn installed_extension_exposes_and_invokes_history_notes_tools() -> TestRe
})
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&requests[1].body)?,
json!({
"context": {
"session_id": "session-123",
"current_agent_name": "/root/worker",
}
})
);
for result in [
json!({"text": ""}),
json!({"text": "x".repeat(4_001)}),
json!({"encrypted_output": "old-backend-hint"}),
] {
server.reset().await;
Mock::given(method("POST"))
.and(path("/backend-api/codex/alpha/notes/v2/thread_hint"))
.respond_with(ResponseTemplate::new(200).set_body_json(result))
.mount(&server)
.await;
assert!(
registry.context_contributors()[0]
.contribute_thread_context(&session_store, &thread_store)
.await
.is_empty()
);
}
let mut disabled_config = config.clone();
disabled_config.token_budget = None;
for contributor in registry.config_contributors() {
contributor.on_config_changed(&session_store, &thread_store, &config, &disabled_config);
}
assert!(exposed_tools(&registry, &session_store, &thread_store).is_empty());
assert!(
registry.context_contributors()[0]
.contribute_thread_context(&session_store, &thread_store)
.await
.is_empty()
);
Ok(())
}