@@ -400,7 +400,7 @@ struct AppRow {
400400 last_attempt_at : Option < String > ,
401401}
402402
403- /// Reconcile published app containers — restart any that should be running but aren't .
403+ /// Reconcile published app containers — pull images and restart any that should be running.
404404async fn reconcile_apps ( db : & Arc < Database > , docker : & DockerManager ) {
405405 let apps: Vec < AppRow > = {
406406 let conn = db. conn ( ) ;
@@ -431,7 +431,10 @@ async fn reconcile_apps(db: &Arc<Database>, docker: &DockerManager) {
431431 for app in apps {
432432 let app_id = & app. id ;
433433 let agent_id = & app. agent_id ;
434- let container_name = format ! ( "app-{app_id}" ) ;
434+ // Container name must match what docker.launch() produces: "xpressclaw-{name}"
435+ // So we pass "app-{app_id}" to launch, and check "xpressclaw-app-{app_id}" for running.
436+ let launch_name = format ! ( "app-{app_id}" ) ;
437+ let container_name = format ! ( "xpressclaw-{launch_name}" ) ;
435438 if docker. is_container_running ( & container_name) . await {
436439 // Running — reset restart count if it was elevated
437440 if app. restart_count > 0 {
@@ -458,10 +461,36 @@ async fn reconcile_apps(db: &Arc<Database>, docker: &DockerManager) {
458461 let image = app. image . unwrap_or_else ( || "node:20-alpine" . to_string ( ) ) ;
459462 let app_port = app. port as u16 ;
460463
464+ // Pull image if not available locally
465+ if !docker. has_image ( & image) . await {
466+ info ! ( app_id, image = image. as_str( ) , "pulling app image" ) ;
467+ match docker. pull_image ( & image) . await {
468+ Ok ( _) => {
469+ info ! ( app_id, image = image. as_str( ) , "app image pulled" ) ;
470+ }
471+ Err ( e) => {
472+ warn ! (
473+ app_id,
474+ image = image. as_str( ) ,
475+ error = %e,
476+ "failed to pull app image, skipping"
477+ ) ;
478+ // Record attempt so backoff works
479+ let conn = db. conn ( ) ;
480+ let _ = conn. execute (
481+ "UPDATE apps SET restart_count = restart_count + 1, last_attempt_at = CURRENT_TIMESTAMP WHERE id = ?1" ,
482+ [ app_id] ,
483+ ) ;
484+ continue ;
485+ }
486+ }
487+ }
488+
461489 info ! (
462490 app_id,
491+ image = image. as_str( ) ,
463492 restart_count = app. restart_count,
464- "restarting app container"
493+ "starting app container"
465494 ) ;
466495
467496 let volume_name = format ! ( "xpressclaw-workspace-{agent_id}" ) ;
@@ -490,7 +519,7 @@ async fn reconcile_apps(db: &Arc<Database>, docker: &DockerManager) {
490519 ) ;
491520 }
492521
493- match docker. launch ( & container_name , & spec) . await {
522+ match docker. launch ( & launch_name , & spec) . await {
494523 Ok ( info) => {
495524 let conn = db. conn ( ) ;
496525 let _ = conn. execute (
@@ -500,11 +529,11 @@ async fn reconcile_apps(db: &Arc<Database>, docker: &DockerManager) {
500529 info ! (
501530 app_id,
502531 container_id = & info. container_id[ ..12 ] ,
503- "app restarted "
532+ "app started "
504533 ) ;
505534 }
506535 Err ( e) => {
507- warn ! ( app_id, error = %e, "failed to restart app" ) ;
536+ warn ! ( app_id, error = %e, "failed to start app container " ) ;
508537 let conn = db. conn ( ) ;
509538 let _ = conn. execute (
510539 "UPDATE apps SET status = 'error', updated_at = CURRENT_TIMESTAMP WHERE id = ?1" ,
0 commit comments