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;
  } */
};