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.

22 changes: 22 additions & 0 deletions crates/graphql-orm-ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,28 @@ 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.14] - 2026-09-01

Persistent schema module: **0.64.0** (unchanged from 0.95.13).

### Fixed

- The strict Codex actor now admits the documented generic `warning`
notification during an exact retained `thread/resume` handshake as well as
during a correlated turn. Warning content is discarded, count and bytes are
bounded independently for each resume or turn window, an optional thread ID
must match, and the notification never advances resume readiness.

### Security

- Warnings remain rejected during new-thread creation, deletion, state-index
scans, and idle periods. A retained turn still requires the correlated
resume response plus matching started notification, or the reviewed
content-free usage fallback.

There is no database, data, table, column, index, constraint, backfill,
protected-payload, GraphQL SDL, backup, or restore migration.

## [0.95.13] - 2026-09-01

Persistent schema module: **0.64.0** (unchanged from 0.95.12).
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.13"
version = "0.95.14"
edition = "2024"
authors = ["Toby Martin <toby@dastari.net>"]
description = "Project-agnostic AI agent runtime for graphql-orm applications"
Expand Down
17 changes: 17 additions & 0 deletions crates/graphql-orm-ai/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@ 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.13 to 0.95.14: retained-resume warning interleaving

Adopt `graphql-orm-ai` 0.95.14 from one reviewed full monorepo revision. A host
waiting for `retained_resume_ready` may receive
`AiCodexAppServerInbound::RuntimeWarning` while the exact `thread/resume`
handshake is pending. Treat it as a content-free diagnostic and continue
reading; it is not resume evidence and does not replace the response, matching
started notification, or reviewed usage fallback.

The actor discards warning text, applies independent count and byte ceilings
to each resume or turn window, and rejects mismatched-thread, malformed,
new-thread, deletion, state-index, and idle warnings. No host configuration or
public data model changes.

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.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
Expand Down
9 changes: 8 additions & 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.13", 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.14", default-features = false, features = ["sqlite"] }
```

Exactly one persistence backend is required: `sqlite` (default), `postgres`,
Expand Down Expand Up @@ -224,6 +224,13 @@ thread state, supplied as a trusted model instruction, and independently
enforced by the actor. Reverify both direct delivery and the negative native-
item matrix before upgrading Codex.

Codex may emit its documented generic `warning` notification while a retained
thread is resuming. The actor accepts it only inside the exact protected
resume window or a correlated turn, discards its message, and enforces strict
per-window count and byte limits. It does not satisfy resume readiness; hosts
must continue reading until the correlated response and started notification,
or the reviewed content-free usage fallback, complete the lifecycle.

The Codex schema projector preserves bounded nullable scalar `type` arrays in
the crate-authored FixedBroker definitions. It does not pass through arbitrary
JSON Schema unions: only unique combinations of supported scalar types plus
Expand Down
6 changes: 4 additions & 2 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.13` with AI schema module
`graphql-orm-ai` is at crate version `0.95.14` 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 @@ -60,7 +60,9 @@ verification evidence belongs in the focused guides.
notification in either order, or the reviewed content-free usage-snapshot
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
requires exact policy equality. Content-discarded, flood-bounded generic
warnings may interleave only within that exact resume window or a correlated
turn and never become readiness evidence. 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,
Expand Down
179 changes: 164 additions & 15 deletions crates/graphql-orm-ai/src/providers/codex_app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ const MAXIMUM_BOOTSTRAP_INSTRUCTION_BYTES: usize = 64 * 1024;
const MAXIMUM_IDENTIFIER_BYTES: usize = 200;
const MAXIMUM_VERSION_BYTES: usize = 200;
const MAXIMUM_RUNTIME_WARNING_MESSAGE_BYTES: usize = 4 * 1024;
const MAXIMUM_RUNTIME_WARNING_BYTES_PER_TURN: usize = 16 * 1024;
const MAXIMUM_RUNTIME_WARNINGS_PER_TURN: usize = 8;
const MAXIMUM_RUNTIME_WARNING_BYTES_PER_WINDOW: usize = 16 * 1024;
const MAXIMUM_RUNTIME_WARNINGS_PER_WINDOW: usize = 8;
const MAXIMUM_CAPABILITY_SESSION_BINDINGS: usize = 256;
const MAXIMUM_WEB_SEARCH_RESULTS_PER_CALL: usize = 100;
const MAXIMUM_WEB_SEARCH_RESULT_BYTES_PER_CALL: usize = 1024 * 1024;
Expand Down Expand Up @@ -3530,7 +3530,8 @@ enum ThreadLifecycleOperation {
/// failure may be followed in a separate fresh actor by the exact bounded,
/// content-discarding active-and-archived state-index absence scan. A
/// documented generic `warning` is admitted only as a content-free,
/// turn-correlated, flood-bounded control event. Empty reasoning-item
/// exact-resume- or turn-correlated, flood-bounded control event. A warning
/// never advances resume readiness. Empty reasoning-item
/// lifecycles are admitted only as content-free signals while turn-level
/// reasoning summaries remain explicitly disabled. All other server-initiated
/// requests and non-allowlisted notifications fail closed.
Expand Down Expand Up @@ -3738,13 +3739,17 @@ impl AiCodexAppServerProtocolActor {
self.thread_lifecycle_phase = ThreadLifecyclePhase::AwaitingResponseAndStarted;
self.thread_lifecycle_operation = Some(ThreadLifecycleOperation::Start);
self.retained_usage_snapshot_observed = false;
self.runtime_warning_count = 0;
self.runtime_warning_bytes = 0;
}

fn begin_resume_lifecycle(&mut self, thread_id: &str) {
self.active_thread_id = Some(thread_id.to_owned());
self.thread_lifecycle_phase = ThreadLifecyclePhase::AwaitingResponseAndStarted;
self.thread_lifecycle_operation = Some(ThreadLifecycleOperation::Resume);
self.retained_usage_snapshot_observed = false;
self.runtime_warning_count = 0;
self.runtime_warning_bytes = 0;
}

/// Encodes an ephemeral thread start with trusted instructions kept in the
Expand Down Expand Up @@ -5099,26 +5104,47 @@ impl AiCodexAppServerProtocolActor {
.runtime_warning_bytes
.checked_add(message_bytes)
.ok_or(ProviderError::Rejected)?;
if notification.method != RUNTIME_WARNING
|| !self.initialization_complete
|| self.thread_lifecycle_phase != ThreadLifecyclePhase::Complete
|| self.pending_turn_thread_id.as_deref() != Some(active_thread_id)
|| self.deleting_thread_id.is_some()
|| (!self
let pending_resume_response = self
.pending
.values()
.any(|method| *method == ClientMethod::ThreadResume);
let resume_correlated = self.thread_lifecycle_operation
== Some(ThreadLifecycleOperation::Resume)
&& self.pending_turn_thread_id.is_none()
&& self.active_turn_id.is_none()
&& match self.thread_lifecycle_phase {
ThreadLifecyclePhase::AwaitingResponseAndStarted
| ThreadLifecyclePhase::AwaitingResponse => {
pending_resume_response && self.pending.len() == 1
}
ThreadLifecyclePhase::AwaitingStarted => self.pending.is_empty(),
ThreadLifecyclePhase::Complete => {
self.retained_usage_snapshot_observed && self.pending.is_empty()
}
ThreadLifecyclePhase::Ready | ThreadLifecyclePhase::Deleted => false,
};
let turn_correlated = self.thread_lifecycle_phase == ThreadLifecyclePhase::Complete
&& self.pending_turn_thread_id.as_deref() == Some(active_thread_id)
&& (self
.pending
.values()
.any(|method| *method == ClientMethod::TurnStart)
&& !self.turn_response_observed
&& !self.turn_started_observed)
|| self.turn_response_observed
|| self.turn_started_observed);
if notification.method != RUNTIME_WARNING
|| !self.initialization_complete
|| self.deleting_thread_id.is_some()
|| self.thread_absence_scan.is_some()
|| (!resume_correlated && !turn_correlated)
|| params
.thread_id
.as_deref()
.is_some_and(|thread_id| thread_id != active_thread_id)
|| params.message.trim().is_empty()
|| message_bytes > MAXIMUM_RUNTIME_WARNING_MESSAGE_BYTES
|| params.message.chars().any(char::is_control)
|| self.runtime_warning_count >= MAXIMUM_RUNTIME_WARNINGS_PER_TURN
|| next_bytes > MAXIMUM_RUNTIME_WARNING_BYTES_PER_TURN
|| self.runtime_warning_count >= MAXIMUM_RUNTIME_WARNINGS_PER_WINDOW
|| next_bytes > MAXIMUM_RUNTIME_WARNING_BYTES_PER_WINDOW
{
return Err(ProviderError::Rejected);
}
Expand Down Expand Up @@ -11613,7 +11639,7 @@ pub(crate) mod tests {
));

let mut count_limited = active_protocol_actor();
for _ in 0..MAXIMUM_RUNTIME_WARNINGS_PER_TURN {
for _ in 0..MAXIMUM_RUNTIME_WARNINGS_PER_WINDOW {
assert!(matches!(
count_limited.accept(&runtime_warning_notification(None, "bounded")),
Ok(AiCodexAppServerInbound::RuntimeWarning)
Expand All @@ -11626,7 +11652,8 @@ pub(crate) mod tests {

let mut byte_limited = active_protocol_actor();
let maximum_message = "x".repeat(MAXIMUM_RUNTIME_WARNING_MESSAGE_BYTES);
for _ in 0..(MAXIMUM_RUNTIME_WARNING_BYTES_PER_TURN / MAXIMUM_RUNTIME_WARNING_MESSAGE_BYTES)
for _ in
0..(MAXIMUM_RUNTIME_WARNING_BYTES_PER_WINDOW / MAXIMUM_RUNTIME_WARNING_MESSAGE_BYTES)
{
assert!(matches!(
byte_limited.accept(&runtime_warning_notification(None, &maximum_message)),
Expand Down Expand Up @@ -12573,6 +12600,128 @@ pub(crate) mod tests {
));
}

#[test]
fn protocol_admits_bounded_runtime_warning_in_every_exact_resume_interleaving() {
let cursor =
crate::AiProviderSessionCursor::new("codex.app_server.thread.v2", "thread-retained-1")
.expect("cursor should validate");
let warning = runtime_warning_notification(
Some("thread-retained-1"),
"A bounded runtime feature remains unavailable.",
);

let mut warning_first = initialized_protocol_actor();
warning_first
.resume_thread(&cursor, &turn())
.expect("warning-first resume should begin");
assert!(matches!(
warning_first.accept(&warning),
Ok(AiCodexAppServerInbound::RuntimeWarning)
));
assert!(!warning_first.retained_resume_ready(&cursor));
warning_first
.accept(br#"{"id":2,"result":{"thread":{"id":"thread-retained-1"}}}"#)
.expect("response should remain required after warning");
assert!(!warning_first.retained_resume_ready(&cursor));
warning_first
.accept(&thread_started_notification("thread-retained-1"))
.expect("started notification should complete warning-first resume");
assert!(warning_first.retained_resume_ready(&cursor));

let mut response_first = initialized_protocol_actor();
response_first
.resume_thread(&cursor, &turn())
.expect("response-first resume should begin");
response_first
.accept(br#"{"id":2,"result":{"thread":{"id":"thread-retained-1"}}}"#)
.expect("response should bind");
assert!(matches!(
response_first.accept(&runtime_warning_without_timestamp(
None,
"The schema permits this warning timestamp to be absent.",
)),
Ok(AiCodexAppServerInbound::RuntimeWarning)
));
assert!(!response_first.retained_resume_ready(&cursor));
response_first
.accept(&thread_started_notification("thread-retained-1"))
.expect("started notification should remain required");
assert!(response_first.retained_resume_ready(&cursor));

let mut started_first = initialized_protocol_actor();
started_first
.resume_thread(&cursor, &turn())
.expect("started-first resume should begin");
started_first
.accept(&thread_started_notification("thread-retained-1"))
.expect("started notification should bind first");
assert!(matches!(
started_first.accept(&warning),
Ok(AiCodexAppServerInbound::RuntimeWarning)
));
assert!(!started_first.retained_resume_ready(&cursor));
started_first
.accept(br#"{"id":2,"result":{"thread":{"id":"thread-retained-1"}}}"#)
.expect("response should remain required after warning");
assert!(started_first.retained_resume_ready(&cursor));
}

#[test]
fn protocol_keeps_resume_warnings_closed_outside_the_exact_resume_window() {
let cursor =
crate::AiProviderSessionCursor::new("codex.app_server.thread.v2", "thread-retained-1")
.expect("cursor should validate");
let warning = runtime_warning_notification(
Some("thread-retained-1"),
"A bounded runtime feature remains unavailable.",
);

let mut mismatched = initialized_protocol_actor();
mismatched
.resume_thread(&cursor, &turn())
.expect("mismatch fixture should begin resume");
assert!(matches!(
mismatched.accept(&runtime_warning_notification(
Some("thread-other"),
"A warning for another thread must not cross this window.",
)),
Err(ProviderError::Rejected)
));
assert!(!mismatched.retained_resume_ready(&cursor));

let mut new_thread = initialized_protocol_actor();
new_thread
.start_persistent_empty_thread(
"model-1",
ModelReasoningEffort::Unspecified,
&trusted_bootstrap(),
&[],
)
.expect("new-thread fixture should begin");
assert!(matches!(
new_thread.accept(&runtime_warning_notification(None, "Not a resume window.")),
Err(ProviderError::Rejected)
));

let mut deleting = initialized_protocol_actor();
deleting
.resume_thread(&cursor, &turn())
.expect("delete fixture resume should begin");
deleting
.accept(br#"{"id":2,"result":{"thread":{"id":"thread-retained-1"}}}"#)
.expect("delete fixture response should bind");
deleting
.accept(&thread_started_notification("thread-retained-1"))
.expect("delete fixture started should bind");
deleting
.delete_thread(&cursor)
.expect("delete fixture should enter deletion window");
assert!(matches!(
deleting.accept(&warning),
Err(ProviderError::Rejected)
));
}

#[test]
fn protocol_accepts_timestamped_thread_started_in_either_correlated_order() {
let mut response_first = initialized_protocol_actor();
Expand Down
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.13` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` |
| `graphql-orm-ai` | `0.95.14` | `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
Loading