diff --git a/Cargo.lock b/Cargo.lock index 3bc54f4..0ab4fec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3104,7 +3104,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai" -version = "0.95.12" +version = "0.95.13" dependencies = [ "agql-auth", "async-graphql", diff --git a/crates/graphql-orm-ai/CHANGELOG.md b/crates/graphql-orm-ai/CHANGELOG.md index 78bdbf7..21b0798 100644 --- a/crates/graphql-orm-ai/CHANGELOG.md +++ b/crates/graphql-orm-ai/CHANGELOG.md @@ -18,6 +18,20 @@ checkpoint facts. For the current workspace baseline and active gates, use the [implementation status](docs/implementation-status.md) and the central [AI production-readiness plan](../../docs/plans/active/ai-production-readiness/README.md). +## [0.95.13] - 2026-09-01 + +Persistent schema module: **0.64.0** (unchanged from 0.95.12). + +### Fixed + +- A freshly initialized Codex process actor can now adopt the exact frozen + web-search policy carried by a retained session while encoding + `thread/resume`. An actor that already owns retained state continues to + reject any domain-policy or call-ceiling change. + +There is no database, data, table, column, index, constraint, backfill, +protected-payload, GraphQL SDL, backup, or restore migration. + ## [0.95.12] - 2026-09-01 Persistent schema module: **0.64.0** (unchanged from 0.95.11). diff --git a/crates/graphql-orm-ai/Cargo.toml b/crates/graphql-orm-ai/Cargo.toml index 652b063..f870f6f 100644 --- a/crates/graphql-orm-ai/Cargo.toml +++ b/crates/graphql-orm-ai/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graphql-orm-ai" -version = "0.95.12" +version = "0.95.13" edition = "2024" authors = ["Toby Martin "] description = "Project-agnostic AI agent runtime for graphql-orm applications" diff --git a/crates/graphql-orm-ai/MIGRATION.md b/crates/graphql-orm-ai/MIGRATION.md index 6aeabfa..bde4c3d 100644 --- a/crates/graphql-orm-ai/MIGRATION.md +++ b/crates/graphql-orm-ai/MIGRATION.md @@ -19,6 +19,17 @@ they describe. For the current workspace baseline and active delivery gates, use [implementation status](docs/implementation-status.md) and the central [AI production-readiness plan](../../docs/plans/active/ai-production-readiness/README.md). +## 0.95.12 to 0.95.13: retained web-search adoption on a fresh process + +Adopt `graphql-orm-ai` 0.95.13 from one reviewed full monorepo revision. No +host API or configuration change is required. A newly initialized process +actor now adopts the retained session's exact web-search domain policy and +call ceiling when it encodes `thread/resume`; an actor with existing retained +state still requires exact equality. + +The AI schema module remains **0.64.0**. There is no database, data, GraphQL +SDL, protected-payload, backup, restore, or data backfill migration. + ## 0.95.11 to 0.95.12: retained-resume readiness and pre-dispatch proof Adopt `graphql-orm-ai` 0.95.12 and `graphql-orm-ai-tool-profiles` 0.10.3 from diff --git a/crates/graphql-orm-ai/README.md b/crates/graphql-orm-ai/README.md index 43cf2ab..c20ba44 100644 --- a/crates/graphql-orm-ai/README.md +++ b/crates/graphql-orm-ai/README.md @@ -28,7 +28,7 @@ for AI, ORM, storage, backup, and tool-profile packages: ```toml [dependencies] -graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.95.12", default-features = false, features = ["sqlite"] } +graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.95.13", default-features = false, features = ["sqlite"] } ``` Exactly one persistence backend is required: `sqlite` (default), `postgres`, diff --git a/crates/graphql-orm-ai/docs/implementation-status.md b/crates/graphql-orm-ai/docs/implementation-status.md index b68e8ba..382418c 100644 --- a/crates/graphql-orm-ai/docs/implementation-status.md +++ b/crates/graphql-orm-ai/docs/implementation-status.md @@ -10,7 +10,7 @@ supersedes: [] # Implementation Status -`graphql-orm-ai` is at crate version `0.95.12` with AI schema module +`graphql-orm-ai` is at crate version `0.95.13` with AI schema module `0.64.0`. It uses workspace `graphql-orm` `0.30.0`, backend-neutral `graphql-orm-ai-tool-profiles` `0.10.3`, and external `agql-auth` `0.19.0` at `1d2e9fe2e1576105212a7b340a11abf8cad0382d`. @@ -58,9 +58,11 @@ verification evidence belongs in the focused guides. controls that cannot become model output or current-run usage. Retained resume readiness requires its correlated response and matching started notification in either order, or the reviewed content-free usage-snapshot - fallback. A typed failure proven before business `turn/start` releases its - reservation, fences cleanup, and remains safely retryable; later failures - retain uncertain-effect recovery. Retained + fallback. A fresh process actor adopts the exact retained web-search policy + while encoding `thread/resume`; an actor with existing retained state still + requires exact policy equality. A typed failure proven before business + `turn/start` releases its reservation, fences cleanup, and remains safely + retryable; later failures retain uncertain-effect recovery. Retained developer instructions are compile-time static, registration-fingerprinted, and distinct from request input. Reasoning effort is a separately typed, model-profile-validated turn field frozen into each retained-session diff --git a/crates/graphql-orm-ai/src/providers/codex_app_server.rs b/crates/graphql-orm-ai/src/providers/codex_app_server.rs index fd55218..598e173 100644 --- a/crates/graphql-orm-ai/src/providers/codex_app_server.rs +++ b/crates/graphql-orm-ai/src/providers/codex_app_server.rs @@ -3898,6 +3898,7 @@ impl AiCodexAppServerProtocolActor { input.validate()?; self.validate_thread_lifecycle_boundary()?; let input_instruction_fingerprint = input.instruction_fingerprint()?; + let has_bound_retained_state = self.retained_model.is_some(); if cursor.kind() != "codex.app_server.thread.v2" || !valid_reference(cursor.expose_to_provider_adapter()) || match self.thread_lifecycle_phase { @@ -3923,12 +3924,13 @@ impl AiCodexAppServerProtocolActor { || !self.pending_dynamic_requests.is_empty() || !self.started_dynamic_calls.is_empty() || !self.responded_dynamic_calls.is_empty() - || self.thread_web_search_domain_policy - != input - .web_search() - .map(|search| search.domain_policy().clone()) - || self.thread_web_search_maximum_calls - != input.web_search().map(|search| search.maximum_calls()) + || (has_bound_retained_state + && (self.thread_web_search_domain_policy + != input + .web_search() + .map(|search| search.domain_policy().clone()) + || self.thread_web_search_maximum_calls + != input.web_search().map(|search| search.maximum_calls()))) { return Err(ProviderError::Rejected); } @@ -11960,6 +11962,81 @@ pub(crate) mod tests { )); } + #[test] + fn fresh_actor_adopts_retained_web_search_state_but_bound_actor_rejects_changes() { + let bootstrap = bootstrap_instructions(); + let domains = ModelWebSearchDomainPolicy::allowed_domains(vec!["example.com".to_owned()]) + .expect("allow-domain policy should validate"); + let request = ModelRequest { + instructions: Vec::new(), + continuation_mode: ModelContinuationMode::ProviderRetained, + tools: vec![dynamic_tool()], + builtin_tools: vec![ModelBuiltinTool::WebSearch { + domains: domains.clone(), + }], + maximum_builtin_tool_calls: Some(2), + ..model_request() + }; + let input = AiCodexAppServerTurnInput::try_from_retained_dynamic_request( + request.clone(), + &bootstrap, + ) + .expect("retained web-search input should validate"); + let cursor = crate::AiProviderSessionCursor::new( + "codex.app_server.thread.v2", + "thread-retained-web-search", + ) + .expect("retained cursor should validate"); + + let mut fresh_actor = initialized_protocol_actor(); + let resume: Value = serde_json::from_slice( + &fresh_actor + .resume_thread(&cursor, &input) + .expect("a fresh process actor should adopt the retained web-search state"), + ) + .expect("resume frame should decode"); + assert_eq!( + resume.pointer("/params/config/tools.web_search.allowed_domains"), + Some(&json!(["example.com"])) + ); + fresh_actor + .accept(br#"{"id":2,"result":{"thread":{"id":"thread-retained-web-search"}}}"#) + .expect("resume response should bind"); + fresh_actor + .accept(&thread_started_notification("thread-retained-web-search")) + .expect("resume notification should bind"); + assert!(fresh_actor.retained_resume_ready(&cursor)); + fresh_actor + .start_turn("thread-retained-web-search", &input) + .expect("the adopted web-search state should admit the retained turn"); + + let mut changed = request; + changed.maximum_builtin_tool_calls = Some(3); + let changed_input = + AiCodexAppServerTurnInput::try_from_retained_dynamic_request(changed, &bootstrap) + .expect("changed web-search input remains structurally valid"); + let mut bound_actor = initialized_protocol_actor(); + bound_actor + .start_persistent_empty_thread_with_web_search( + input.model(), + input.reasoning_effort(), + &bootstrap, + input.tools(), + input.web_search(), + ) + .expect("retained web-search thread should encode"); + bound_actor + .accept(br#"{"id":2,"result":{"thread":{"id":"thread-retained-web-search"}}}"#) + .expect("thread response should bind"); + bound_actor + .accept(&thread_started_notification("thread-retained-web-search")) + .expect("thread notification should bind"); + assert!(matches!( + bound_actor.resume_thread(&cursor, &changed_input), + Err(ProviderError::Rejected) + )); + } + #[test] fn native_web_search_lifecycle_surfaces_results_and_enforces_turn_ceiling() { let policy = ModelWebSearchDomainPolicy::allowed_domains(vec!["example.com".to_owned()]) diff --git a/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock b/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock index 9dd305c..4fcb928 100644 --- a/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock +++ b/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock @@ -1329,7 +1329,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai" -version = "0.95.12" +version = "0.95.13" dependencies = [ "agql-auth", "async-graphql", diff --git a/docs/reference/workspace-packages.md b/docs/reference/workspace-packages.md index ec2bf8b..e4da89a 100644 --- a/docs/reference/workspace-packages.md +++ b/docs/reference/workspace-packages.md @@ -19,7 +19,7 @@ changes. | Package | Version | Path | Default features | Direct internal dependencies | | --- | --- | --- | --- | --- | | `graphql-orm` | `0.30.0` | `crates/graphql-orm` | `sqlite` | `graphql-orm-macros`, `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | -| `graphql-orm-ai` | `0.95.12` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | +| `graphql-orm-ai` | `0.95.13` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | | `graphql-orm-ai-tool-profiles` | `0.10.3` | `crates/graphql-orm-ai-tool-profiles` | none | `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | | `graphql-orm-backup` | `0.7.2` | `crates/graphql-orm-backup` | `local` | `graphql-orm` (optional), `graphql-orm-storage` | | `graphql-orm-macros` | `0.30.0` | `crates/graphql-orm-macros` | `sqlite` | none |