feat(workstate): réconcilie le live-state au reboot (use case ReconcileLiveState)
Au redémarrage, les agents qui étaient `working` lors de la fermeture restaient figés en statut fantôme `working` alors qu'aucun tour n'est plus en cours (bug reproductible en live : QA et DevBackend eux-mêmes sont restés `working` fantôme après leurs tâches). On réconcilie l'état persistant à l'ouverture du projet : les statuts orphelins sont ramenés à la cible `idle`. - domain/live_state.rs : const STALE_AT_RESTART_MARKER + `reconcile_orphans` (+ tests). - application/workstate/reconcile.rs (nouveau) : use case ReconcileLiveState (pas de nouveau port), best-effort, no-op si store vide (+ tests, dont empty_store_is_a_noop_at_boot). - application/workstate/mod.rs + lib.rs : module + re-export. - app-tauri/state.rs : provider par-root + champ AppState + wiring. - app-tauri/commands.rs : hook best-effort dans open_project. QA VERT par commande réelle : cargo test --workspace 1624 passed / 0 failed, cargo clippy --workspace --all-targets 0 erreur sur le code ajouté. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -19,6 +19,7 @@ use application::{
|
||||
ListResumableAgentsInput, ListSkillsInput, LiveSessions, LoadLayoutInput, McpRuntime,
|
||||
MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, ReadConversationPageInput,
|
||||
ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput,
|
||||
ReconcileLiveStateInput,
|
||||
RenameLayoutInput, ResolveAgentPermissionsInput, ResolveMemoryLinksInput,
|
||||
RotateConversationLogInput, SetActiveLayoutInput, SnapshotRunningAgentsInput,
|
||||
StopLiveAgentInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput,
|
||||
@ -124,6 +125,16 @@ pub async fn open_project(
|
||||
.reconcile_layouts
|
||||
.execute(ReconcileLayoutsInput { project_id: id })
|
||||
.await;
|
||||
// Réconcilie les lignes de live-state fantômes : un crash ne déclenche jamais
|
||||
// `close_project`, donc l'ouverture est le seul filet fiable pour repasser en
|
||||
// `idle` les agents `working`/`waiting`/`blocked` dont la session est morte.
|
||||
// Acte système (appelle le port live-state directement, jamais via la surface
|
||||
// MCP self-only). Best-effort : un échec ne bloque pas l'ouverture. Tourne
|
||||
// AVANT toute relance d'agent (toute reprise réécrit ensuite en LWW).
|
||||
let _ = state
|
||||
.reconcile_live_state
|
||||
.execute(ReconcileLiveStateInput { project_id: id })
|
||||
.await;
|
||||
// (Re)start the orchestrator watcher for this project (idempotent, §14.3).
|
||||
state.ensure_orchestrator_watch(&output.project);
|
||||
state.reconcile_claude_run_dirs(&output.project).await;
|
||||
|
||||
@ -27,7 +27,8 @@ use application::{
|
||||
McpRuntime, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
|
||||
OrchestratorService, PermissionProjectorRegistry, ProposeContext, ReadAgentContext,
|
||||
ReadContext, ReadConversationPage, ReadMemory, ReadMemoryIndex, ReadProjectContext, ReadSkill,
|
||||
RecallMemory, ReconcileLayouts, RecordTurn, RecordTurnProvider, ReferenceProfiles,
|
||||
RecallMemory, ReconcileLayouts, ReconcileLiveState, ReconcileLiveStateInput, RecordTurn,
|
||||
RecordTurnProvider, ReferenceProfiles,
|
||||
RenameLayout, ResizeTerminal, ResolveAgentPermissions, ResolveMemoryLinks,
|
||||
RotateConversationLog, SaveEmbedderProfile, SaveProfile, SessionLimitService, SetActiveLayout,
|
||||
SnapshotRunningAgents, StopLiveAgent, StructuredSessions, SuggestedThisSession,
|
||||
@ -212,6 +213,43 @@ impl LiveStateReadProvider for AppLiveStateLeanProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider par-root de la réconciliation du live-state au reboot (jumeau de
|
||||
/// [`AppLiveStateProvider`] côté système). Résout le **project root** depuis le
|
||||
/// `project_id` via le [`ProjectStore`], matérialise un [`FsLiveStateStore`] ciblant
|
||||
/// `<root>/.ideai/live-state.json`, puis exécute le use case applicatif
|
||||
/// [`ReconcileLiveState`] (qui croise chaque ligne avec la liveness réelle de
|
||||
/// [`LiveSessions`] et ré-upsert les fantômes en `idle`).
|
||||
///
|
||||
/// **Acte système** : appelle le port [`domain::ports::LiveStateStore`]
|
||||
/// **directement**, jamais via `OrchestratorCommand::SetWorkState` ⇒ le self-only de
|
||||
/// `idea_workstate_set` reste préservé par construction. Best-effort : le call site
|
||||
/// (`open_project`) ignore le résultat.
|
||||
pub(crate) struct AppReconcileLiveState {
|
||||
projects: Arc<dyn ProjectStore>,
|
||||
registry: Arc<LiveSessions>,
|
||||
clock: Arc<dyn Clock>,
|
||||
}
|
||||
|
||||
impl AppReconcileLiveState {
|
||||
/// Résout la racine du projet, lie le store par-root et lance la réconciliation.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError`] si le projet est inconnu (registre) ou si le store échoue.
|
||||
pub(crate) async fn execute(
|
||||
&self,
|
||||
input: ReconcileLiveStateInput,
|
||||
) -> Result<(), AppError> {
|
||||
let project = self.projects.load_project(input.project_id).await?;
|
||||
let store = Arc::new(FsLiveStateStore::new(&project.root));
|
||||
let uc = ReconcileLiveState::new(
|
||||
store,
|
||||
Arc::clone(&self.registry) as Arc<dyn LiveAgentRegistry>,
|
||||
Arc::clone(&self.clock),
|
||||
);
|
||||
uc.execute(input).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Implémente [`ProviderSessionProvider`](application::ProviderSessionProvider) (lot
|
||||
/// P8b) en matérialisant un [`FsProviderSessionStore`] ciblant le **project root** du
|
||||
/// lancement en cours.
|
||||
@ -383,6 +421,10 @@ pub struct AppState {
|
||||
/// Dé-doublonne, à l'ouverture, les feuilles d'agent en double d'un même
|
||||
/// agent dans `layouts.json` (R0c). Idempotent : no-op sans doublon.
|
||||
pub reconcile_layouts: Arc<ReconcileLayouts>,
|
||||
/// Réconcilie, à l'ouverture, les lignes de live-state fantômes (agent
|
||||
/// `working`/`waiting`/`blocked` dont la session n'est plus vivante) : les
|
||||
/// repasse en `idle`. Acte système best-effort, jamais via la surface MCP.
|
||||
pub(crate) reconcile_live_state: Arc<AppReconcileLiveState>,
|
||||
/// Detect which candidate profiles' CLIs are installed (first-run).
|
||||
pub detect_profiles: Arc<DetectProfiles>,
|
||||
/// List configured profiles.
|
||||
@ -1169,6 +1211,16 @@ impl AppState {
|
||||
Arc::clone(&terminal_sessions),
|
||||
Arc::clone(&structured_sessions),
|
||||
));
|
||||
// Réconciliation du live-state au reboot : repasse en `idle` les lignes
|
||||
// fantômes (working/waiting/blocked) dont la session n'est plus vivante,
|
||||
// selon le MÊME registre de liveness que `GetProjectWorkState`. Provider
|
||||
// par root (le store fixe sa racine à la construction). Câblé dans
|
||||
// `open_project`, best-effort (cf. `reconcile_layouts`).
|
||||
let reconcile_live_state = Arc::new(AppReconcileLiveState {
|
||||
projects: Arc::clone(&store_port),
|
||||
registry: Arc::clone(&live_sessions),
|
||||
clock: Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
});
|
||||
// Lot C — résumés de conversation best-effort : on câble les sources par
|
||||
// project root (handoff = primaire, log = repli `last(_, 3)`). Lecture seule,
|
||||
// aucune persistance ; un échec de preview ne bloque ni live/busy ni tickets.
|
||||
@ -1374,6 +1426,7 @@ impl AppState {
|
||||
set_active_layout,
|
||||
snapshot_running_agents,
|
||||
reconcile_layouts,
|
||||
reconcile_live_state,
|
||||
detect_profiles,
|
||||
list_profiles,
|
||||
save_profile,
|
||||
|
||||
Reference in New Issue
Block a user