Add a native cloud backend to the app-server client (#59493)

## What changed

Expose `cloud_backend::connect` to adapt an authenticated `codex-cloud-client` client to `AppServerClient`. Support account reads, resuming a conversation by ID with saved settings, reading its metadata, listing the loaded thread, and unsubscribing.

Forward turn lifecycle events and agent message deltas, and track thread status from attachment notifications. Keep the previous attachment when a replacement fails, report stream disconnections, and reject turns without an active attachment. Unsupported requests and approval replies return explicit errors.

## Testing

Add tests covering failed attachment replacement, turn admission after disconnect, and thread status tracking across notifications and reattachment.

GitOrigin-RevId: 5a131e814d588cadeb9fb94513b99e92b47e3da4
This commit is contained in:
Benjamin Carlsson
2026-11-04 20:51:55 +00:00
committed by copyberry
parent 08bbbbdff3
commit 5112ce6ede
10 changed files with 554 additions and 1 deletions
+1 -1
View File
@@ -482,7 +482,7 @@ fn parse_subagent_source(value: wire::SubAgentSource) -> Result<SubAgentSource,
}
}
fn parse_status(value: wire::ThreadStatus) -> Result<ThreadStatus, Error> {
pub(super) fn parse_status(value: wire::ThreadStatus) -> Result<ThreadStatus, Error> {
match required(value.value)? {
wire::thread_status::Value::NotLoaded(_) => Ok(ThreadStatus::NotLoaded),
wire::thread_status::Value::Idle(_) => Ok(ThreadStatus::Idle),
+22
View File
@@ -1,5 +1,7 @@
//! Typed live transcript updates for the attached thread.
use crate::Error;
use crate::Event;
use crate::ThreadStatus;
use crate::transcript_wire as wire;
use std::fmt;
@@ -97,6 +99,7 @@ redacted_debug!(
);
// Field numbers in the ASB Notification.value oneof.
const THREAD_STATUS_CHANGED: u32 = 3;
const TURN_STARTED: u32 = 21;
const TURN_COMPLETED: u32 = 23;
pub(super) const ITEM_STARTED: u32 = 27;
@@ -120,6 +123,25 @@ impl From<prost::DecodeError> for DecodeFailure {
pub(super) fn required<T>(value: Option<T>) -> Result<T, DecodeFailure> {
value.ok_or(DecodeFailure::MissingField)
}
impl Event {
/// Returns a supported status update for this thread from an Attach notification.
pub fn thread_status_for(&self, thread_id: &str) -> Option<ThreadStatus> {
let Self::Notification(bytes) = self else {
return None;
};
let (tag, values) = wire::notification(bytes).ok().flatten()?;
if tag != THREAD_STATUS_CHANGED {
return None;
}
let envelope = wire::merge_messages::<wire::Envelope<wire::StatusUpdate>>(values).ok()?;
let params = envelope.params?;
if params.thread_id.as_deref() != Some(thread_id) {
return None;
}
crate::activation::parse_status(params.status?).ok()
}
}
impl TranscriptEvent {
pub(crate) fn decode(thread_id: &str, payload: &[u8]) -> Result<Option<Self>, Error> {
match Self::decode_present_fields(thread_id, payload) {
@@ -67,6 +67,13 @@ pub(super) struct Envelope<T: Message + Default + std::fmt::Debug> {
pub params: Option<T>,
}
#[derive(Clone, Message)]
pub(super) struct StatusUpdate {
#[prost(message, optional, tag = "1")]
pub status: Option<crate::activation_wire::ThreadStatus>,
#[prost(string, optional, tag = "2")]
pub thread_id: Option<String>,
}
#[derive(Clone, Message)]
pub(super) struct PatchUpdate {
#[prost(message, optional, tag = "1")]
pub changes: Option<Changes>,