fix(domain,application,infrastructure,app-tauri,frontend): conversation_id redevient un pivot logique IdeA/log/handoff, reprise provider réservée à la sélection explicite — lifecycle: LaunchAgentInput::conversation_id documenté comme sélection explicite (Some = reprise native si disponible sinon handoff portable, None = conversation provider neuve), plus de copie implicite du pivot logique de la leaf au reboot ; fallback silencieux: provider_resume_fallback résolu via resolve_handoff et câblé jusqu'aux adaptateurs Claude/Codex/OpenCode via AgentSessionFactory::create resume_fallback_preamble + with_resume_fallback_preamble — le préambule reste dormant si la session native répond et n'est injecté que si la session provider s'avère introuvable/périmée (fake stale-then-fresh en test, relance fraîche sans erreur utilisateur) ; app-tauri launch_agent: contrat documenté, l'activation ordinaire d'une cellule doit envoyer None même si la leaf conserve un pivot logique d'avant reboot ; frontend CustomAgentChatView: freshOnOpen réinitialise l'état conversation complet (scrollback, view state, conversationId publié, bannière reprise, pagination historique) quand le runtime structuré n'existe plus au remontage — pas de fausse « Conversation reprise » après reboot ; tests: structured_launch_d3 étendu, structured_session_d0, agent_lifecycle structured, CustomAgentChatView freshOnOpen — #372 (QA verte: cargo check --tests -p domain -p application -p infrastructure, cargo test -p application --test structured_launch_d3, cargo test -p domain --test structured_session_d0, cargo test -p application --test agent_lifecycle structured, npm test -- CustomAgentChatView, npm run typecheck, git diff --check ; fichiers .ideai/* et scripts/ hors périmètre ; aucun push)
This commit is contained in:
@ -25,7 +25,10 @@ use domain::ports::{
|
||||
use domain::sandbox::{SandboxEnforcer, SandboxPlan};
|
||||
use domain::SessionId;
|
||||
|
||||
use super::process::{run_turn_cancellable, SpawnLine, TurnCancel};
|
||||
use super::process::{
|
||||
is_missing_provider_session, resume_fallback_prompt, run_turn_cancellable, SpawnLine,
|
||||
TurnCancel,
|
||||
};
|
||||
|
||||
/// Résultat du parsing d'une ligne : **zéro ou plusieurs** événements à émettre et/ou
|
||||
/// un `session_id` capté (init/result). Permet à [`parse_event`] de rester **pure**
|
||||
@ -300,6 +303,8 @@ pub struct ClaudeSdkSession {
|
||||
cwd: String,
|
||||
/// Id de conversation **du moteur** Claude, capté au premier tour, `None` avant.
|
||||
conversation_id: Mutex<Option<String>>,
|
||||
/// Portable handoff injected only if the seeded provider session is stale.
|
||||
resume_fallback_preamble: Option<String>,
|
||||
/// Plan de sandbox OS **par lancement** (lot LP4-4), porté dans chaque
|
||||
/// [`SpawnLine`]. `None` ⇒ aucun sandboxing (drain async natif).
|
||||
sandbox: Option<SandboxPlan>,
|
||||
@ -330,12 +335,20 @@ impl ClaudeSdkSession {
|
||||
command: command.into(),
|
||||
cwd: cwd.into(),
|
||||
conversation_id: Mutex::new(seed_conversation_id),
|
||||
resume_fallback_preamble: None,
|
||||
sandbox,
|
||||
sandbox_enforcer,
|
||||
turn_cancel: TurnCancel::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the portable context used by the one-shot stale-resume fallback.
|
||||
#[must_use]
|
||||
pub fn with_resume_fallback_preamble(mut self, preamble: Option<&str>) -> Self {
|
||||
self.resume_fallback_preamble = preamble.map(str::to_owned);
|
||||
self
|
||||
}
|
||||
|
||||
/// Compose la ligne de commande d'un tour selon l'état de conversation.
|
||||
///
|
||||
/// Format RÉEL vérifié 2026-06-09 :
|
||||
@ -408,6 +421,7 @@ impl ClaudeSdkSession {
|
||||
prompt: &str,
|
||||
tap: Option<std::sync::mpsc::Sender<ReplyEvent>>,
|
||||
) -> Result<ReplyStream, AgentSessionError> {
|
||||
let had_native_resume = self.conversation_id().is_some();
|
||||
let spec = self.build_spawn_line(prompt);
|
||||
let (line_tap, parser_thread) = tap.map_or((None, None), |event_tap| {
|
||||
let (line_tx, line_rx) = std::sync::mpsc::channel::<String>();
|
||||
@ -424,14 +438,27 @@ impl ClaudeSdkSession {
|
||||
});
|
||||
(Some(line_tx), Some(parser))
|
||||
});
|
||||
let raw_result = run_turn_cancellable(
|
||||
let mut raw_result = run_turn_cancellable(
|
||||
&spec,
|
||||
None,
|
||||
self.sandbox_enforcer.as_ref(),
|
||||
line_tap,
|
||||
line_tap.clone(),
|
||||
&self.turn_cancel,
|
||||
)
|
||||
.await;
|
||||
if had_native_resume && raw_result.as_ref().is_err_and(is_missing_provider_session) {
|
||||
*self.conversation_id.lock().expect("mutex sain") = None;
|
||||
let fallback_prompt =
|
||||
resume_fallback_prompt(self.resume_fallback_preamble.as_deref(), prompt);
|
||||
raw_result = run_turn_cancellable(
|
||||
&self.build_spawn_line(&fallback_prompt),
|
||||
None,
|
||||
self.sandbox_enforcer.as_ref(),
|
||||
line_tap,
|
||||
&self.turn_cancel,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if let Some(parser) = parser_thread {
|
||||
let _ = parser.join();
|
||||
}
|
||||
|
||||
@ -22,7 +22,10 @@ use domain::ports::{
|
||||
use domain::sandbox::{SandboxEnforcer, SandboxPlan};
|
||||
use domain::SessionId;
|
||||
|
||||
use super::process::{run_turn_cancellable, SpawnLine, TurnCancel};
|
||||
use super::process::{
|
||||
is_missing_provider_session, resume_fallback_prompt, run_turn_cancellable, SpawnLine,
|
||||
TurnCancel,
|
||||
};
|
||||
|
||||
/// Résultat du parsing d'une ligne Codex : **zéro ou plusieurs** événements et/ou un
|
||||
/// id de conversation Codex capté. Miroir de `claude::ParsedLine` (vecteur d'events
|
||||
@ -401,6 +404,8 @@ pub struct CodexExecSession {
|
||||
env: Vec<(String, String)>,
|
||||
/// Id de conversation **du moteur** Codex, capté au premier tour, `None` avant.
|
||||
conversation_id: Mutex<Option<String>>,
|
||||
/// Portable handoff injected only if the seeded provider session is stale.
|
||||
resume_fallback_preamble: Option<String>,
|
||||
/// Plan de sandbox OS **par lancement** (lot LP4-4), porté dans chaque
|
||||
/// [`SpawnLine`]. `None` ⇒ aucun sandboxing (drain async natif).
|
||||
sandbox: Option<SandboxPlan>,
|
||||
@ -503,12 +508,20 @@ impl CodexExecSession {
|
||||
model_reasoning_effort,
|
||||
env,
|
||||
conversation_id: Mutex::new(seed_conversation_id),
|
||||
resume_fallback_preamble: None,
|
||||
sandbox,
|
||||
sandbox_enforcer,
|
||||
turn_cancel: TurnCancel::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the portable context used by the one-shot stale-resume fallback.
|
||||
#[must_use]
|
||||
pub fn with_resume_fallback_preamble(mut self, preamble: Option<&str>) -> Self {
|
||||
self.resume_fallback_preamble = preamble.map(str::to_owned);
|
||||
self
|
||||
}
|
||||
|
||||
/// Compose la ligne de commande d'un tour.
|
||||
///
|
||||
/// Format RÉEL vérifié 2026-06-10 (codex 0.137.0) :
|
||||
@ -641,6 +654,7 @@ impl CodexExecSession {
|
||||
prompt: &str,
|
||||
tap: Option<std::sync::mpsc::Sender<ReplyEvent>>,
|
||||
) -> Result<ReplyStream, AgentSessionError> {
|
||||
let had_native_resume = self.conversation_id().is_some();
|
||||
let spec = self.build_spawn_line(prompt);
|
||||
let has_tap = tap.is_some();
|
||||
let (line_tap, parser_thread) = tap.map_or((None, None), |event_tap| {
|
||||
@ -670,14 +684,27 @@ impl CodexExecSession {
|
||||
});
|
||||
(Some(line_tx), Some(parser))
|
||||
});
|
||||
let raw_result = run_turn_cancellable(
|
||||
let mut raw_result = run_turn_cancellable(
|
||||
&spec,
|
||||
None,
|
||||
self.sandbox_enforcer.as_ref(),
|
||||
line_tap,
|
||||
line_tap.clone(),
|
||||
&self.turn_cancel,
|
||||
)
|
||||
.await;
|
||||
if had_native_resume && raw_result.as_ref().is_err_and(is_missing_provider_session) {
|
||||
*self.conversation_id.lock().expect("mutex sain") = None;
|
||||
let fallback_prompt =
|
||||
resume_fallback_prompt(self.resume_fallback_preamble.as_deref(), prompt);
|
||||
raw_result = run_turn_cancellable(
|
||||
&self.build_spawn_line(&fallback_prompt),
|
||||
None,
|
||||
self.sandbox_enforcer.as_ref(),
|
||||
line_tap,
|
||||
&self.turn_cancel,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if let Some(parser) = parser_thread {
|
||||
let _ = parser.join();
|
||||
}
|
||||
|
||||
@ -142,6 +142,7 @@ impl AgentSessionFactory for StructuredSessionFactory {
|
||||
ctx: &PreparedContext,
|
||||
cwd: &ProjectPath,
|
||||
session: &SessionPlan,
|
||||
resume_fallback_preamble: Option<&str>,
|
||||
requester: Option<&str>,
|
||||
env: &[(String, String)],
|
||||
sandbox: Option<&SandboxPlan>,
|
||||
@ -175,9 +176,10 @@ impl AgentSessionFactory for StructuredSessionFactory {
|
||||
// injection supplémentaire n'incombe ici en mode structuré (la CLI lit son
|
||||
// fichier conventionnel — CLAUDE.md / AGENTS.md — depuis le cwd).
|
||||
let session: Arc<dyn AgentSession> = match adapter {
|
||||
StructuredAdapter::Claude => Arc::new(ClaudeSdkSession::new(
|
||||
id, command, cwd, seed, plan, enforcer,
|
||||
)),
|
||||
StructuredAdapter::Claude => Arc::new(
|
||||
ClaudeSdkSession::new(id, command, cwd, seed, plan, enforcer)
|
||||
.with_resume_fallback_preamble(resume_fallback_preamble),
|
||||
),
|
||||
StructuredAdapter::Codex => {
|
||||
let policy = match structured_policy {
|
||||
Some(StructuredProviderLaunchPolicy::Codex {
|
||||
@ -195,32 +197,38 @@ impl AgentSessionFactory for StructuredSessionFactory {
|
||||
));
|
||||
}
|
||||
};
|
||||
Arc::new(CodexExecSession::new_with_policy_and_overrides(
|
||||
Arc::new(
|
||||
CodexExecSession::new_with_policy_and_overrides(
|
||||
id,
|
||||
command,
|
||||
cwd,
|
||||
seed,
|
||||
policy.0,
|
||||
policy.1,
|
||||
policy.2,
|
||||
profile.model.clone(),
|
||||
profile.model_reasoning_effort.clone(),
|
||||
env.to_vec(),
|
||||
plan,
|
||||
enforcer,
|
||||
)
|
||||
.with_resume_fallback_preamble(resume_fallback_preamble),
|
||||
)
|
||||
}
|
||||
StructuredAdapter::OpenCode => Arc::new(
|
||||
OpenCodeSession::new(
|
||||
id,
|
||||
command,
|
||||
cwd,
|
||||
profile.command.clone(),
|
||||
profile.args.clone(),
|
||||
cwd.clone(),
|
||||
ctx.project_root.clone(),
|
||||
seed,
|
||||
policy.0,
|
||||
policy.1,
|
||||
policy.2,
|
||||
profile.model.clone(),
|
||||
profile.model_reasoning_effort.clone(),
|
||||
env.to_vec(),
|
||||
plan,
|
||||
enforcer,
|
||||
))
|
||||
}
|
||||
StructuredAdapter::OpenCode => Arc::new(OpenCodeSession::new(
|
||||
id,
|
||||
profile.command.clone(),
|
||||
profile.args.clone(),
|
||||
cwd.clone(),
|
||||
ctx.project_root.clone(),
|
||||
seed,
|
||||
env.to_vec(),
|
||||
plan,
|
||||
enforcer,
|
||||
)?),
|
||||
)?
|
||||
.with_resume_fallback_preamble(resume_fallback_preamble),
|
||||
),
|
||||
StructuredAdapter::OpenAiCompatible => {
|
||||
let config = profile.chat_http.clone().ok_or_else(|| {
|
||||
AgentSessionError::Start(format!(
|
||||
|
||||
@ -577,6 +577,7 @@ mod tests {
|
||||
&cwd(),
|
||||
&SessionPlan::None,
|
||||
None,
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
@ -601,6 +602,7 @@ mod tests {
|
||||
&cwd(),
|
||||
&SessionPlan::None,
|
||||
None,
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
Some(&codex_policy),
|
||||
@ -634,6 +636,7 @@ mod tests {
|
||||
&temp_cwd("factory-openai"),
|
||||
&SessionPlan::None,
|
||||
None,
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
@ -672,6 +675,7 @@ mod tests {
|
||||
&cwd(),
|
||||
&SessionPlan::None,
|
||||
None,
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
@ -723,6 +727,7 @@ mod tests {
|
||||
&cwd(),
|
||||
&SessionPlan::None,
|
||||
None,
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
Some(&policy),
|
||||
@ -762,6 +767,7 @@ mod tests {
|
||||
conversation_id: "repris-42".to_owned(),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
@ -916,6 +922,89 @@ mod tests {
|
||||
(bin.to_string_lossy().into_owned(), argv)
|
||||
}
|
||||
|
||||
/// Fake provider that rejects its first (resume) invocation as stale, then
|
||||
/// succeeds as a fresh Codex conversation on the retry.
|
||||
fn make_stale_then_fresh_codex_fake() -> (String, std::path::PathBuf) {
|
||||
use std::io::Write as _;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
static C: AtomicU64 = AtomicU64::new(0);
|
||||
let n = C.fetch_add(1, Ordering::Relaxed);
|
||||
let dir = std::env::current_dir()
|
||||
.expect("cwd")
|
||||
.join("target")
|
||||
.join("test-fakes")
|
||||
.join("session");
|
||||
std::fs::create_dir_all(&dir).expect("create stale fake dir");
|
||||
let bin = dir.join(format!("idea-stale-cli-{}-{n}", std::process::id()));
|
||||
let argv = dir.join(format!("idea-stale-argv-{}-{n}", std::process::id()));
|
||||
let marker = dir.join(format!("idea-stale-marker-{}-{n}", std::process::id()));
|
||||
let script = format!(
|
||||
"#!/bin/sh\nfor a in \"$@\"; do printf '%s\\n' \"$a\" >> '{}'; done\n\
|
||||
if [ ! -f '{}' ]; then touch '{}'; printf '%s\\n' 'session not found' >&2; exit 1; fi\n\
|
||||
printf '%s\\n' '{{\"type\":\"thread.started\",\"thread_id\":\"fresh-thread\"}}'\n\
|
||||
printf '%s\\n' '{{\"type\":\"item.completed\",\"item\":{{\"id\":\"i0\",\"type\":\"agent_message\",\"text\":\"fallback ok\"}}}}'\n",
|
||||
argv.display(),
|
||||
marker.display(),
|
||||
marker.display(),
|
||||
);
|
||||
let mut file = std::fs::File::create(&bin).expect("create stale fake");
|
||||
file.write_all(script.as_bytes()).expect("write stale fake");
|
||||
file.sync_all().expect("sync stale fake");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut permissions = std::fs::metadata(&bin).unwrap().permissions();
|
||||
permissions.set_mode(0o755);
|
||||
std::fs::set_permissions(&bin, permissions).unwrap();
|
||||
}
|
||||
super::conformance::wait_until_executable(&bin);
|
||||
(bin.to_string_lossy().into_owned(), argv)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stale_codex_resume_retries_fresh_with_handoff_without_surface_error() {
|
||||
let factory = StructuredSessionFactory::new();
|
||||
let (command, argv) = make_stale_then_fresh_codex_fake();
|
||||
let profile = structured_profile(StructuredAdapter::Codex, &command);
|
||||
let policy = StructuredProviderLaunchPolicy::Codex {
|
||||
sandbox_mode: "read-only".to_owned(),
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
};
|
||||
let session = factory
|
||||
.start(
|
||||
&profile,
|
||||
&prepared_ctx(),
|
||||
&cwd(),
|
||||
&SessionPlan::Resume {
|
||||
conversation_id: "stale-thread".to_owned(),
|
||||
},
|
||||
Some("Résumé portable conservé."),
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
Some(&policy),
|
||||
)
|
||||
.await
|
||||
.expect("session adapter starts");
|
||||
|
||||
let stream = session
|
||||
.send("Continuer le travail")
|
||||
.await
|
||||
.expect("stale provider resume falls back silently");
|
||||
assert!(stream.into_iter().any(|event| matches!(
|
||||
event,
|
||||
ReplyEvent::Final { content } if content == "fallback ok"
|
||||
)));
|
||||
assert_eq!(session.conversation_id().as_deref(), Some("fresh-thread"));
|
||||
|
||||
let recorded = std::fs::read_to_string(argv).expect("recorded argv");
|
||||
assert!(recorded.contains("resume\nstale-thread"));
|
||||
assert!(recorded.contains("Contexte borné de la conversation reprise"));
|
||||
assert!(recorded.contains("Résumé portable conservé."));
|
||||
assert!(recorded.contains("Continuer le travail"));
|
||||
}
|
||||
|
||||
// ---- Claude parse_event : plusieurs blocs, robustesse ---------------
|
||||
|
||||
/// Messages successifs : chaque message produit ses événements ; ici un par
|
||||
@ -1848,6 +1937,7 @@ mod tests {
|
||||
conversation_id: "cx-resume".to_owned(),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
Some(&policy),
|
||||
@ -1874,6 +1964,7 @@ mod tests {
|
||||
conversation_id: "ignored-by-engine".to_owned(),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
@ -1907,6 +1998,7 @@ mod tests {
|
||||
&cwd(),
|
||||
&SessionPlan::None,
|
||||
None,
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
|
||||
@ -23,7 +23,7 @@ use domain::ports::{
|
||||
use domain::sandbox::{SandboxEnforcer, SandboxPlan};
|
||||
use domain::SessionId;
|
||||
|
||||
use super::process::{TurnCancel, TurnLease};
|
||||
use super::process::{is_missing_provider_session, resume_fallback_prompt, TurnCancel, TurnLease};
|
||||
|
||||
/// Un événement OpenCode parsé depuis stdout JSONL.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@ -555,6 +555,8 @@ pub struct OpenCodeSession {
|
||||
project_root: String,
|
||||
env: Vec<(String, String)>,
|
||||
engine_session_id: Mutex<Option<String>>,
|
||||
/// Portable handoff injected only if the seeded provider session is stale.
|
||||
resume_fallback_preamble: Option<String>,
|
||||
turn_cancel: TurnCancel,
|
||||
sandbox: Option<SandboxPlan>,
|
||||
sandbox_enforcer: Option<Arc<dyn SandboxEnforcer>>,
|
||||
@ -585,12 +587,20 @@ impl OpenCodeSession {
|
||||
project_root: project_root.into(),
|
||||
env,
|
||||
engine_session_id: Mutex::new(seed),
|
||||
resume_fallback_preamble: None,
|
||||
turn_cancel: TurnCancel::new(),
|
||||
sandbox,
|
||||
sandbox_enforcer,
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds the portable context used by the one-shot stale-resume fallback.
|
||||
#[must_use]
|
||||
pub fn with_resume_fallback_preamble(mut self, preamble: Option<&str>) -> Self {
|
||||
self.resume_fallback_preamble = preamble.map(str::to_owned);
|
||||
self
|
||||
}
|
||||
|
||||
fn build_args(&self, prompt: &str, engine_session_id: Option<&str>) -> Vec<String> {
|
||||
let mut args = self.prefix_args.clone();
|
||||
args.extend(["run".to_owned(), "--format".to_owned(), "json".to_owned()]);
|
||||
@ -663,9 +673,16 @@ impl OpenCodeSession {
|
||||
Ok(events) => events,
|
||||
Err(err)
|
||||
if initial_session.is_some()
|
||||
&& matches!(err, AgentSessionError::Decode(ref message) if opencode_missing_final(message)) =>
|
||||
&& (matches!(err, AgentSessionError::Decode(ref message) if opencode_missing_final(message))
|
||||
|| is_missing_provider_session(&err)) =>
|
||||
{
|
||||
self.run_once(prompt, tap, None, &turn).await?
|
||||
*self
|
||||
.engine_session_id
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
|
||||
let fallback_prompt =
|
||||
resume_fallback_prompt(self.resume_fallback_preamble.as_deref(), prompt);
|
||||
self.run_once(&fallback_prompt, tap, None, &turn).await?
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
@ -722,11 +722,59 @@ fn process_exit_message(command: &str, code: Option<i32>, stderr: &[u8]) -> Stri
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a provider rejected a native resume because its remote/local session
|
||||
/// no longer exists. Kept deliberately narrow: authentication, permissions and
|
||||
/// generic process failures must remain visible instead of being retried.
|
||||
pub(super) fn is_missing_provider_session(error: &AgentSessionError) -> bool {
|
||||
let AgentSessionError::Io(message) = error else {
|
||||
return false;
|
||||
};
|
||||
let message = message.to_ascii_lowercase();
|
||||
[
|
||||
"session not found",
|
||||
"thread not found",
|
||||
"conversation not found",
|
||||
"no session found",
|
||||
"no thread found",
|
||||
"no conversation found",
|
||||
"unknown session",
|
||||
"unknown thread",
|
||||
"no rollout found with id",
|
||||
]
|
||||
.iter()
|
||||
.any(|needle| message.contains(needle))
|
||||
}
|
||||
|
||||
pub(super) fn resume_fallback_prompt(preamble: Option<&str>, prompt: &str) -> String {
|
||||
preamble.map_or_else(
|
||||
|| prompt.to_owned(),
|
||||
|summary| format!(
|
||||
"Contexte borné de la conversation reprise :\n\n{summary}\n\n---\n\nNouveau message utilisateur :\n{prompt}"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Instant;
|
||||
|
||||
#[test]
|
||||
fn missing_provider_session_classifier_is_narrow() {
|
||||
assert!(is_missing_provider_session(&AgentSessionError::Io(
|
||||
"codex exited with code 1: session not found".to_owned()
|
||||
)));
|
||||
assert!(is_missing_provider_session(&AgentSessionError::Io(
|
||||
"no rollout found with id abc".to_owned()
|
||||
)));
|
||||
assert!(!is_missing_provider_session(&AgentSessionError::Io(
|
||||
"permission denied".to_owned()
|
||||
)));
|
||||
assert!(!is_missing_provider_session(&AgentSessionError::Start(
|
||||
"session not found".to_owned()
|
||||
)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_turn_cancellable_kills_current_child_without_waiting_for_natural_exit() {
|
||||
let spec = SpawnLine {
|
||||
|
||||
@ -476,6 +476,7 @@ async fn structured_sandboxed_turn_preserves_conversation_id() {
|
||||
&cwd,
|
||||
&SessionPlan::None,
|
||||
None,
|
||||
None,
|
||||
&[],
|
||||
Some(&plan),
|
||||
None,
|
||||
|
||||
@ -496,6 +496,7 @@ impl AgentSessionFactory for BlockingReplyFactory {
|
||||
_ctx: &PreparedContext,
|
||||
_cwd: &ProjectPath,
|
||||
_session: &SessionPlan,
|
||||
_resume_fallback_preamble: Option<&str>,
|
||||
_requester: Option<&str>,
|
||||
_env: &[(String, String)],
|
||||
_sandbox: Option<&domain::sandbox::SandboxPlan>,
|
||||
|
||||
Reference in New Issue
Block a user