diff --git a/backend/ControlCenter/ControlCenter.csproj b/backend/ControlCenter/ControlCenter.csproj
index 66ebc83..b688abd 100644
--- a/backend/ControlCenter/ControlCenter.csproj
+++ b/backend/ControlCenter/ControlCenter.csproj
@@ -11,6 +11,8 @@
+
+
diff --git a/backend/ControlCenter/Data/Configurations/AgentStateConfiguration.cs b/backend/ControlCenter/Data/Configurations/AgentStateConfiguration.cs
new file mode 100644
index 0000000..96df065
--- /dev/null
+++ b/backend/ControlCenter/Data/Configurations/AgentStateConfiguration.cs
@@ -0,0 +1,69 @@
+using ControlCenter.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ControlCenter.Data.Configurations;
+
+///
+/// EF Core entity configuration for .
+///
+/// Maps to the agents table with snake_case column naming,
+/// consistent with the Extrudex project conventions.
+///
+/// CUB-56 will create the migration; this configuration only
+/// defines the entity-to-table mapping and constraints.
+///
+public class AgentStateConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ // Table name
+ builder.ToTable("agents");
+
+ // Primary key
+ builder.HasKey(a => a.Id);
+
+ // Properties with snake_case column names
+ builder.Property(a => a.Id)
+ .HasColumnName("id")
+ .ValueGeneratedOnAdd();
+
+ builder.Property(a => a.Status)
+ .HasColumnName("status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasDefaultValue("idle");
+
+ builder.Property(a => a.Task)
+ .HasColumnName("task")
+ .IsRequired(false)
+ .HasMaxLength(500);
+
+ builder.Property(a => a.Progress)
+ .HasColumnName("progress")
+ .IsRequired(false);
+
+ builder.Property(a => a.SessionKey)
+ .HasColumnName("session_key")
+ .IsRequired()
+ .HasMaxLength(255);
+
+ builder.Property(a => a.Channel)
+ .HasColumnName("channel")
+ .IsRequired()
+ .HasMaxLength(50);
+
+ builder.Property(a => a.LastActivity)
+ .HasColumnName("last_activity")
+ .IsRequired()
+ .HasDefaultValueSql("NOW()");
+
+ // Indexes
+ builder.HasIndex(a => a.SessionKey)
+ .IsUnique()
+ .HasDatabaseName("ix_agents_session_key");
+
+ builder.HasIndex(a => a.Status)
+ .HasDatabaseName("ix_agents_status");
+ }
+}
\ No newline at end of file
diff --git a/backend/ControlCenter/Data/ControlCenterDbContext.cs b/backend/ControlCenter/Data/ControlCenterDbContext.cs
new file mode 100644
index 0000000..1a7fd63
--- /dev/null
+++ b/backend/ControlCenter/Data/ControlCenterDbContext.cs
@@ -0,0 +1,39 @@
+using ControlCenter.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace ControlCenter.Data;
+
+///
+/// EF Core DbContext for the Control Center database.
+///
+/// Provides access to all persistent entities.
+/// Uses snake_case naming conventions for PostgreSQL columns,
+/// applied via .
+///
+/// Connection string is configured in appsettings.json
+/// under ConnectionStrings:ControlCenterDb.
+///
+public class ControlCenterDbContext : DbContext
+{
+ ///
+ /// Agent state records — one row per active or recently active agent.
+ /// Maps to the agents table.
+ ///
+ public DbSet AgentStates => Set();
+
+ public ControlCenterDbContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ // Apply all entity configurations from this assembly
+ modelBuilder.ApplyConfigurationsFromAssembly(
+ typeof(Configurations.AgentStateConfiguration).Assembly);
+
+ // Global snake_case naming convention for any unmapped columns
+ // (explicit configurations take precedence)
+ base.OnModelCreating(modelBuilder);
+ }
+}
\ No newline at end of file
diff --git a/backend/ControlCenter/Models/AgentState.cs b/backend/ControlCenter/Models/AgentState.cs
new file mode 100644
index 0000000..500cb20
--- /dev/null
+++ b/backend/ControlCenter/Models/AgentState.cs
@@ -0,0 +1,59 @@
+namespace ControlCenter.Models;
+
+///
+/// Persistent state for an agent in the Control Center.
+/// Maps to the agents table in PostgreSQL.
+///
+/// Tracks the operational status, current task, progress,
+/// session identity, and last activity timestamp for each agent
+/// in the minion fleet.
+///
+/// The property uses string values matching
+/// the AgentStatus enum: "active", "idle", "thinking", "error".
+/// Stored as varchar rather than a DB enum for schema flexibility.
+///
+public class AgentState
+{
+ ///
+ /// Unique identifier for the agent state record.
+ /// Defaults to a new on creation.
+ ///
+ public Guid Id { get; set; } = Guid.NewGuid();
+
+ ///
+ /// Operational status of the agent.
+ /// Valid values: "active", "idle", "thinking", "error".
+ /// Maps to the AgentStatus enum used by SignalR.
+ ///
+ public string Status { get; set; } = "idle";
+
+ ///
+ /// Description of the agent's current task, if any.
+ /// Null when the agent is idle with no active assignment.
+ ///
+ public string? Task { get; set; }
+
+ ///
+ /// Task progress percentage (0–100).
+ /// Null when progress is not trackable for the current task.
+ ///
+ public int? Progress { get; set; }
+
+ ///
+ /// Full session key identifying the agent's active session.
+ /// Format: agent:{agentId}:{channel}:...
+ /// Used to correlate SignalR events with persistent state.
+ ///
+ public string SessionKey { get; set; } = string.Empty;
+
+ ///
+ /// The channel the agent is operating on (e.g., "telegram", "discord", "slack").
+ ///
+ public string Channel { get; set; } = string.Empty;
+
+ ///
+ /// Timestamp of the agent's last recorded activity.
+ /// Updated on every status change or task progress event.
+ ///
+ public DateTime LastActivity { get; set; } = DateTime.UtcNow;
+}
\ No newline at end of file
diff --git a/backend/ControlCenter/Program.cs b/backend/ControlCenter/Program.cs
index 757b20a..aee367d 100644
--- a/backend/ControlCenter/Program.cs
+++ b/backend/ControlCenter/Program.cs
@@ -1,6 +1,9 @@
using System.Reflection;
+using ControlCenter.Data;
using ControlCenter.Hubs;
+using ControlCenter.Repositories;
using ControlCenter.Services;
+using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
@@ -25,6 +28,19 @@ builder.Services.AddSwaggerGen(c =>
}
});
+// ── Database ────────────────────────────────────────────────
+// PostgreSQL via EF Core with Npgsql.
+// Connection string from appsettings.json or environment variable.
+builder.Services.AddDbContext(options =>
+ options.UseNpgsql(
+ builder.Configuration.GetConnectionString("ControlCenterDb")
+ ?? throw new InvalidOperationException(
+ "Connection string 'ControlCenterDb' not found. " +
+ "Add it to appsettings.json or set the environment variable.")));
+
+// ── Repositories ────────────────────────────────────────────
+builder.Services.AddScoped();
+
// ── CORS (kiosk + remote browser) ─────────────────────────
// The Control Center frontend runs on a different origin than the backend.
// SignalR requires credentials for WebSocket transport, so we use
diff --git a/backend/ControlCenter/Repositories/AgentStateRepository.cs b/backend/ControlCenter/Repositories/AgentStateRepository.cs
new file mode 100644
index 0000000..752f8f7
--- /dev/null
+++ b/backend/ControlCenter/Repositories/AgentStateRepository.cs
@@ -0,0 +1,62 @@
+using ControlCenter.Data;
+using ControlCenter.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace ControlCenter.Repositories;
+
+///
+/// EF Core implementation of .
+///
+/// Queries the agents table via
+/// using snake_case column mappings defined in the .
+/// All write operations call SaveChangesAsync immediately.
+///
+public class AgentStateRepository : IAgentStateRepository
+{
+ private readonly ControlCenterDbContext _context;
+ private readonly ILogger _logger;
+
+ public AgentStateRepository(
+ ControlCenterDbContext context,
+ ILogger logger)
+ {
+ _context = context;
+ _logger = logger;
+ }
+
+ ///
+ public async Task> GetAllAsync()
+ {
+ _logger.LogDebug("Fetching all agent states");
+ return await _context.AgentStates.ToListAsync();
+ }
+
+ ///
+ public async Task GetBySessionKeyAsync(string sessionKey)
+ {
+ _logger.LogDebug("Looking up agent state by session key: {SessionKey}", sessionKey);
+ return await _context.AgentStates
+ .FirstOrDefaultAsync(a => a.SessionKey == sessionKey);
+ }
+
+ ///
+ public async Task UpdateStatusAsync(Guid id, string status)
+ {
+ _logger.LogDebug("Updating agent status: {Id} → {Status}", id, status);
+
+ var entity = await _context.AgentStates.FindAsync(id);
+ if (entity is null)
+ {
+ _logger.LogWarning("Agent state not found: {Id}", id);
+ return null;
+ }
+
+ entity.Status = status;
+ entity.LastActivity = DateTime.UtcNow;
+
+ _context.AgentStates.Update(entity);
+ await _context.SaveChangesAsync();
+
+ return entity;
+ }
+}
\ No newline at end of file
diff --git a/backend/ControlCenter/Repositories/IAgentStateRepository.cs b/backend/ControlCenter/Repositories/IAgentStateRepository.cs
new file mode 100644
index 0000000..6097439
--- /dev/null
+++ b/backend/ControlCenter/Repositories/IAgentStateRepository.cs
@@ -0,0 +1,38 @@
+using ControlCenter.Models;
+
+namespace ControlCenter.Repositories;
+
+///
+/// Repository interface for querying and updating agent state.
+///
+/// Provides the data-access contract used by controllers,
+/// background services, and SignalR hubs to read and mutate
+/// persistent agent state.
+///
+/// Implementation should use
+/// via EF Core with PostgreSQL (snake_case columns).
+///
+public interface IAgentStateRepository
+{
+ ///
+ /// Returns all agent states from the database.
+ ///
+ /// A collection of all records.
+ Task> GetAllAsync();
+
+ ///
+ /// Finds an agent state by its session key.
+ ///
+ /// The full session key, e.g. "agent:dex:telegram:direct:...".
+ /// The matching , or null if not found.
+ Task GetBySessionKeyAsync(string sessionKey);
+
+ ///
+ /// Updates the status of an agent state record.
+ /// Also updates LastActivity to .
+ ///
+ /// The unique identifier of the agent state record.
+ /// The new status value ("active", "idle", "thinking", "error").
+ /// The updated , or null if the record was not found.
+ Task UpdateStatusAsync(Guid id, string status);
+}
\ No newline at end of file
diff --git a/backend/ControlCenter/appsettings.json b/backend/ControlCenter/appsettings.json
index 60f801d..25d46d9 100644
--- a/backend/ControlCenter/appsettings.json
+++ b/backend/ControlCenter/appsettings.json
@@ -8,6 +8,10 @@
},
"AllowedHosts": "*",
+ "ConnectionStrings": {
+ "ControlCenterDb": "Host=localhost;Port=5432;Database=control_center;Username=control_center;Password=changeme"
+ },
+
"Gateway": {
"WebSocketUrl": "ws://localhost:3271/ws",
"AuthToken": ""