Spaces:
Running
Running
File size: 7,935 Bytes
2e4b248 b8d04a8 50ca5e6 4f3ec57 50ca5e6 b8d04a8 2e4b248 b8d04a8 2e4b248 b8d04a8 2e4b248 b8d04a8 2e4b248 b8d04a8 2e4b248 2087c2e 2e4b248 ca9803e add19d7 2e4b248 b8d04a8 2e4b248 b8d04a8 2e4b248 b8d04a8 54a7af4 50ca5e6 b8d04a8 2e4b248 b8d04a8 2e4b248 b8d04a8 2e4b248 b8d04a8 70e8361 b8d04a8 2e4b248 b8d04a8 ca9803e add19d7 54a7af4 50ca5e6 b8d04a8 2e4b248 b8d04a8 70e8361 2e4b248 70e8361 2e4b248 70e8361 2e4b248 70e8361 2e4b248 70e8361 f422954 70e8361 2e4b248 70e8361 2e4b248 70e8361 b8d04a8 50ca5e6 2e4b248 b8d04a8 5538a01 f98b943 54a7af4 50ca5e6 5538a01 70e8361 f98b943 5538a01 57fd4b1 70e8361 54a7af4 f98b943 54a7af4 2e4b248 b8d04a8 50ca5e6 54a7af4 b192598 2e4b248 b8d04a8 54a7af4 b8d04a8 2e4b248 b8d04a8 06bf72a b8d04a8 06bf72a 50ca5e6 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
// 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;
} */
}; |