Skip to content

Commit b317c10

Browse files
authored
Merge pull request #93 from XpressAI/fix/xclaw-82-app-reconciler
fix(XCLAW-82): app reconciler — image pull, container name mismatch
2 parents 9b39623 + 8a0325f commit b317c10

1 file changed

Lines changed: 35 additions & 6 deletions

File tree

crates/xpressclaw-core/src/agents/reconciler.rs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
404404
async 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

Comments
 (0)