Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions crates/graphql-orm-ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm-ai/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graphql-orm-ai"
version = "0.95.12"
version = "0.95.13"
edition = "2024"
authors = ["Toby Martin <toby@dastari.net>"]
description = "Project-agnostic AI agent runtime for graphql-orm applications"
Expand Down
11 changes: 11 additions & 0 deletions crates/graphql-orm-ai/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm-ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<reviewed-full-40-character-commit-sha>", version = "0.95.12", default-features = false, features = ["sqlite"] }
graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.95.13", default-features = false, features = ["sqlite"] }
```

Exactly one persistence backend is required: `sqlite` (default), `postgres`,
Expand Down
10 changes: 6 additions & 4 deletions crates/graphql-orm-ai/docs/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down
89 changes: 83 additions & 6 deletions crates/graphql-orm-ai/src/providers/codex_app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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()])
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/reference/workspace-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down