ai_plugin_server / stateManager.js
everydaycats's picture
Update stateManager.js
add19d7 verified
// stateManager.js
let db = null;
export const initDB = (firebaseDB) => {
db = firebaseDB;
};
const activeProjects = new Map();
export const StateManager = {
/**
* GET PROJECT
* Hydrates from DB if not in memory.
* Flattens { info, thumbnail, state } into a usable Memory Object.
*/
getProject: async (projectId) => {
// 1. Try Memory
if (activeProjects.has(projectId)) {
return activeProjects.get(projectId);
}
// 2. Try Firebase (Hydration)
if (db) {
try {
console.log(`[StateManager] 🟡 Hydrating project ${projectId} from DB...`);
// We fetch the root because the Server AI needs EVERYTHING (context, history) to function.
// If this was a client-side Dashboard, we would only fetch /info.
const snapshot = await db.ref(`projects/${projectId}`).once('value');
if (snapshot.exists()) {
const dbData = snapshot.val();
// FLATTEN DB STRUCTURE -> MEMORY STRUCTURE
const memoryObject = {
...dbData.info, // Title, stats, description
thumbnail: dbData.thumbnail?.url || null,
// State might be undefined if just created, so default it
commandQueue: dbData.state?.commandQueue || [],
workerHistory: dbData.state?.workerHistory || [],
pmHistory: dbData.state?.pmHistory || [],
failureCount: dbData.state?.failureCount || 0,
gdd: dbData.state?.gdd || null,
// CRITICAL: Initialize lastActive to NOW so it doesn't expire immediately
lastActive: Date.now(),
lastUpdated: Date.now()
};
// Load into Memory
activeProjects.set(projectId, memoryObject);
console.log(`[StateManager] 🟢 Project ${projectId} hydrated.`);
return memoryObject;
}
} catch (err) {
console.error(`[StateManager] Error reading DB for ${projectId}:`, err);
}
}
return null;
},
/**
* UPDATE PROJECT
* Updates Memory immediately.
* Performs granular updates to DB (separating Info vs State).
*/
updateProject: async (projectId, data) => {
let current = activeProjects.get(projectId);
// Safety check: ensure we have base state
if (!current) {
current = await StateManager.getProject(projectId) || {
commandQueue: [], workerHistory: [], pmHistory: [], failureCount: 0
};
}
const timestamp = Date.now();
// 1. Update Memory (Flat Merge)
const newData = {
...current,
...data,
// CRITICAL: Initialize lastActive to NOW so it doesn't expire immediately
lastActive: Date.now(),
lastUpdated: Date.now()
};
activeProjects.set(projectId, newData);
// 2. Update Firebase (Nested Split)
if (db) {
const updates = {};
// We assume 'data' contains the specific fields that changed.
// However, to be safe and robust, we map the current state to the correct buckets.
// Bucket: Info
if (data.status || data.stats || data.title) {
updates[`projects/${projectId}/info/status`] = newData.status;
if (newData.lastUpdated) updates[`projects/${projectId}/info/lastUpdated`] = newData.lastUpdated;
// Add other info fields if they are mutable
}
// Bucket: State (History, Queue, GDD) - This is the most frequent update
if (data.workerHistory || data.pmHistory || data.commandQueue || data.failureCount || data.gdd) {
updates[`projects/${projectId}/state/workerHistory`] = newData.workerHistory;
updates[`projects/${projectId}/state/pmHistory`] = newData.pmHistory;
updates[`projects/${projectId}/state/commandQueue`] = newData.commandQueue;
updates[`projects/${projectId}/state/failureCount`] = newData.failureCount;
if (newData.gdd) updates[`projects/${projectId}/state/gdd`] = newData.gdd;
}
// Bucket: Thumbnail (Only update if explicitly passed in 'data', avoiding re-upload of massive string)
if (data.thumbnail) {
updates[`projects/${projectId}/thumbnail`] = {url: data.thumbnail};
}
// Perform the update
if (Object.keys(updates).length > 0) {
db.ref().update(updates).catch(err => {
console.error(`[StateManager] 🔴 DB Write Failed for ${projectId}:`, err);
});
}
}
return newData;
},
queueCommand: async (projectId, input) => {
const project = await StateManager.getProject(projectId);
if (!project) return;
let command = null;
if (typeof input === 'object' && input.type && input.payload) {
command = input;
}
else if (typeof input === 'string') {
const rawResponse = input;
if (rawResponse.includes("[ASK_PM:")) return;
if (rawResponse.includes("[GENERATE_IMAGE:") && !rawResponse.includes("```")) return;
const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i);
const readScriptMatch = rawResponse.match(/\[READ_SCRIPT:\s*(.*?)\]/);
const readHierarchyMatch = rawResponse.match(/\[READ_HIERARCHY:\s*(.*?)\]/);
const readLogsMatch = rawResponse.includes("[READ_LOGS]");
if (codeMatch) command = { type: "EXECUTE", payload: codeMatch[1].trim() };
else if (readScriptMatch) command = { type: "READ_SCRIPT", payload: readScriptMatch[1].trim() };
else if (readHierarchyMatch) command = { type: "READ_HIERARCHY", payload: readHierarchyMatch[1].trim() };
else if (readLogsMatch) command = { type: "READ_LOGS", payload: null };
}
if (command) {
project.commandQueue.push(command);
console.log(`[${projectId}] Queued Action: ${command.type}`);
// Sync queue to DB
await StateManager.updateProject(projectId, { commandQueue: project.commandQueue });
}
},
popCommand: async (projectId) => {
const project = await StateManager.getProject(projectId);
if (!project || !project.commandQueue.length) return null;
const command = project.commandQueue.shift();
// Sync queue to DB
await StateManager.updateProject(projectId, { commandQueue: project.commandQueue });
return command;
},
cleanupMemory: () => {
const now = Date.now();
const FOUR_HOURS = 4 * 60 * 60 * 1000;
let count = 0;
for (const [id, data] of activeProjects.entries()) {
// FIX: If lastActive is missing/undefined/0, reset it to NOW.
// This prevents the "54 year old project" bug where (now - 0) > 4 hours.
if (!data.lastActive) {
console.warn(`[StateManager] ⚠️ Project ${id} missing timestamp. Healing data...`);
data.lastActive = now;
continue; // Skip deleting this round
}
if (now - data.lastActive > FOUR_HOURS) {
console.log(`[StateManager] 🧹 Removing expired project: ${id}`);
activeProjects.delete(id);
count++;
}
}
if (count > 0) {
console.log(`[StateManager] 🗑️ Cleaned ${count} projects from memory.`);
}
return count;
}
/* cleanupMemory: () => {
const now = Date.now();
const FOUR_HOURS = 4 * 60 * 60 * 1000;
let count = 0;
for (const [id, data] of activeProjects.entries()) {
if (now - (data.lastActive || 0) > FOUR_HOURS) {
activeProjects.delete(id);
count++;
}
}
console.log(`[StateManager] 🧹 Memory Cleanup: Removed ${count} inactive projects.`);
return count;
} */
};