@jungjaehoon/mama-core 1.0.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +33 -24
- package/src/config-loader.js +0 -218
- package/src/db-adapter/README.md +0 -110
- package/src/db-adapter/base-adapter.js +0 -91
- package/src/db-adapter/index.js +0 -31
- package/src/db-adapter/sqlite-adapter.js +0 -364
- package/src/db-adapter/statement.js +0 -127
- package/src/db-manager.js +0 -679
- package/src/debug-logger.js +0 -86
- package/src/decision-formatter.js +0 -1276
- package/src/decision-tracker.js +0 -621
- package/src/embedding-cache.js +0 -222
- package/src/embedding-client.js +0 -141
- package/src/embedding-server/index.js +0 -424
- package/src/embedding-server/mobile/auth.js +0 -160
- package/src/embedding-server/mobile/daemon.js +0 -313
- package/src/embedding-server/mobile/output-parser.js +0 -281
- package/src/embedding-server/mobile/session-api.js +0 -279
- package/src/embedding-server/mobile/session-manager.js +0 -377
- package/src/embedding-server/mobile/websocket-handler.js +0 -443
- package/src/embeddings.js +0 -316
- package/src/errors.js +0 -326
- package/src/index.js +0 -41
- package/src/mama-api.js +0 -2614
- package/src/memory-inject.js +0 -174
- package/src/memory-store.js +0 -89
- package/src/notification-manager.js +0 -3
- package/src/ollama-client.js +0 -391
- package/src/outcome-tracker.js +0 -351
- package/src/progress-indicator.js +0 -110
- package/src/query-intent.js +0 -237
- package/src/relevance-scorer.js +0 -286
- package/src/tier-validator.js +0 -269
- package/src/time-formatter.js +0 -98
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview WebSocket Handler - MessageRouter-based real-time communication
|
|
3
|
-
* @module mobile/websocket-handler
|
|
4
|
-
* @version 2.0.0
|
|
5
|
-
*
|
|
6
|
-
* Handles WebSocket connections for mobile clients, using MessageRouter
|
|
7
|
-
* for unified message processing (same as Discord/Slack gateways).
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* const { createWebSocketHandler } = require('./websocket-handler');
|
|
11
|
-
* const wsHandler = createWebSocketHandler({
|
|
12
|
-
* httpServer,
|
|
13
|
-
* messageRouter,
|
|
14
|
-
* sessionStore,
|
|
15
|
-
* authToken: process.env.MAMA_AUTH_TOKEN
|
|
16
|
-
* });
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const { WebSocketServer } = require('ws');
|
|
20
|
-
const { authenticateWebSocket } = require('./auth.js');
|
|
21
|
-
const fs = require('fs').promises;
|
|
22
|
-
const path = require('path');
|
|
23
|
-
const os = require('os');
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Default heartbeat interval (30 seconds)
|
|
27
|
-
* @type {number}
|
|
28
|
-
*/
|
|
29
|
-
const HEARTBEAT_INTERVAL = 30000;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Connection timeout (35 seconds - slightly longer than heartbeat)
|
|
33
|
-
* @type {number}
|
|
34
|
-
*/
|
|
35
|
-
const _CONNECTION_TIMEOUT = 35000;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Generate unique client ID
|
|
39
|
-
* @returns {string}
|
|
40
|
-
*/
|
|
41
|
-
function generateClientId() {
|
|
42
|
-
return `client_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Extract user ID from WebSocket URL or headers
|
|
47
|
-
* @param {http.IncomingMessage} req - HTTP upgrade request
|
|
48
|
-
* @returns {string}
|
|
49
|
-
*/
|
|
50
|
-
function extractUserId(req) {
|
|
51
|
-
try {
|
|
52
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
53
|
-
const userId = url.searchParams.get('userId');
|
|
54
|
-
if (userId) {
|
|
55
|
-
return userId;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const ip = req.headers['x-forwarded-for'] || req.socket?.remoteAddress;
|
|
59
|
-
if (!ip) {
|
|
60
|
-
return `user_${Date.now()}`;
|
|
61
|
-
}
|
|
62
|
-
return `user_${ip.replace(/[:.]/g, '_')}`;
|
|
63
|
-
} catch {
|
|
64
|
-
return `user_${Date.now()}`;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Create WebSocket handler with MessageRouter integration
|
|
70
|
-
* @param {Object} options - Configuration options
|
|
71
|
-
* @param {http.Server} options.httpServer - HTTP server to attach WebSocket to
|
|
72
|
-
* @param {MessageRouter} options.messageRouter - Message router instance
|
|
73
|
-
* @param {SessionStore} options.sessionStore - Session store instance
|
|
74
|
-
* @param {string} [options.authToken] - Optional authentication token
|
|
75
|
-
* @returns {WebSocketServer}
|
|
76
|
-
*/
|
|
77
|
-
function createWebSocketHandler({
|
|
78
|
-
httpServer,
|
|
79
|
-
messageRouter,
|
|
80
|
-
sessionStore,
|
|
81
|
-
authToken: _authToken,
|
|
82
|
-
}) {
|
|
83
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
84
|
-
const clients = new Map();
|
|
85
|
-
|
|
86
|
-
httpServer.on('upgrade', (req, socket, head) => {
|
|
87
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
88
|
-
|
|
89
|
-
if (url.pathname !== '/ws') {
|
|
90
|
-
socket.destroy();
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const mockWs = {
|
|
95
|
-
close: (code, reason) => {
|
|
96
|
-
socket.write(`HTTP/1.1 401 Unauthorized\r\n\r\n`);
|
|
97
|
-
socket.destroy();
|
|
98
|
-
console.error(`[WebSocket] Auth failed: ${reason}`);
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
if (!authenticateWebSocket(req, mockWs)) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
107
|
-
wss.emit('connection', ws, req);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
wss.on('connection', (ws, req) => {
|
|
112
|
-
const clientId = generateClientId();
|
|
113
|
-
const userId = extractUserId(req);
|
|
114
|
-
|
|
115
|
-
const clientInfo = {
|
|
116
|
-
clientId,
|
|
117
|
-
userId,
|
|
118
|
-
ws,
|
|
119
|
-
isAlive: true,
|
|
120
|
-
connectedAt: new Date().toISOString(),
|
|
121
|
-
};
|
|
122
|
-
clients.set(clientId, clientInfo);
|
|
123
|
-
|
|
124
|
-
console.error(`[WebSocket] Client ${clientId} connected (user: ${userId})`);
|
|
125
|
-
|
|
126
|
-
ws.send(
|
|
127
|
-
JSON.stringify({
|
|
128
|
-
type: 'connected',
|
|
129
|
-
clientId,
|
|
130
|
-
userId,
|
|
131
|
-
})
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
ws.on('message', (data) => {
|
|
135
|
-
try {
|
|
136
|
-
const message = JSON.parse(data.toString());
|
|
137
|
-
handleClientMessage(clientId, message, clientInfo, messageRouter, sessionStore);
|
|
138
|
-
} catch (err) {
|
|
139
|
-
console.error(`[WebSocket] Invalid message from ${clientId}:`, err.message);
|
|
140
|
-
ws.send(
|
|
141
|
-
JSON.stringify({
|
|
142
|
-
type: 'error',
|
|
143
|
-
error: 'Invalid message format',
|
|
144
|
-
})
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
ws.on('pong', () => {
|
|
150
|
-
clientInfo.isAlive = true;
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
ws.on('close', (code, _reason) => {
|
|
154
|
-
console.error(`[WebSocket] Client ${clientId} disconnected (code: ${code})`);
|
|
155
|
-
clients.delete(clientId);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
ws.on('error', (err) => {
|
|
159
|
-
console.error(`[WebSocket] Error for client ${clientId}:`, err.message);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const heartbeatInterval = setInterval(() => {
|
|
164
|
-
clients.forEach((clientInfo, clientId) => {
|
|
165
|
-
if (!clientInfo.isAlive) {
|
|
166
|
-
console.error(`[WebSocket] Client ${clientId} timed out, terminating`);
|
|
167
|
-
clientInfo.ws.terminate();
|
|
168
|
-
clients.delete(clientId);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
clientInfo.isAlive = false;
|
|
173
|
-
clientInfo.ws.ping();
|
|
174
|
-
});
|
|
175
|
-
}, HEARTBEAT_INTERVAL);
|
|
176
|
-
|
|
177
|
-
wss.on('close', () => {
|
|
178
|
-
clearInterval(heartbeatInterval);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
wss.getClients = () => clients;
|
|
182
|
-
wss.getClientCount = () => clients.size;
|
|
183
|
-
|
|
184
|
-
return wss;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Handle incoming client message
|
|
189
|
-
* @param {string} clientId - Client identifier
|
|
190
|
-
* @param {Object} message - Parsed message object
|
|
191
|
-
* @param {Object} clientInfo - Client info object
|
|
192
|
-
* @param {MessageRouter} messageRouter - Message router instance
|
|
193
|
-
* @param {SessionStore} sessionStore - Session store instance
|
|
194
|
-
*/
|
|
195
|
-
async function handleClientMessage(clientId, message, clientInfo, messageRouter, sessionStore) {
|
|
196
|
-
const { type, content } = message;
|
|
197
|
-
|
|
198
|
-
switch (type) {
|
|
199
|
-
case 'send':
|
|
200
|
-
if (!content) {
|
|
201
|
-
clientInfo.ws.send(
|
|
202
|
-
JSON.stringify({
|
|
203
|
-
type: 'error',
|
|
204
|
-
error: 'Message content required',
|
|
205
|
-
})
|
|
206
|
-
);
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
clientInfo.ws.send(
|
|
212
|
-
JSON.stringify({
|
|
213
|
-
type: 'typing',
|
|
214
|
-
status: 'processing',
|
|
215
|
-
})
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
// Keep-alive: Send periodic status updates to prevent WebSocket timeout
|
|
219
|
-
// Browser may disconnect idle WebSocket after ~30 seconds
|
|
220
|
-
let processingSeconds = 0;
|
|
221
|
-
const keepAliveInterval = setInterval(() => {
|
|
222
|
-
processingSeconds += 10;
|
|
223
|
-
if (clientInfo.ws.readyState === 1) {
|
|
224
|
-
// WebSocket.OPEN
|
|
225
|
-
clientInfo.ws.send(
|
|
226
|
-
JSON.stringify({
|
|
227
|
-
type: 'typing',
|
|
228
|
-
status: 'processing',
|
|
229
|
-
elapsed: processingSeconds,
|
|
230
|
-
})
|
|
231
|
-
);
|
|
232
|
-
console.error(
|
|
233
|
-
`[WebSocket] Keep-alive sent to ${clientId} (${processingSeconds}s elapsed)`
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
}, 10000); // Every 10 seconds
|
|
237
|
-
|
|
238
|
-
const normalizedMessage = {
|
|
239
|
-
source: clientInfo.osAgentMode ? 'viewer' : 'mobile',
|
|
240
|
-
channelId: 'mama_os_main', // Fixed channel for all MAMA OS viewers
|
|
241
|
-
channelName: clientInfo.osAgentMode ? 'MAMA OS' : 'Mobile App', // Human-readable channel name
|
|
242
|
-
userId: clientInfo.userId,
|
|
243
|
-
text: content,
|
|
244
|
-
metadata: {
|
|
245
|
-
clientId,
|
|
246
|
-
sessionId: clientInfo.sessionId,
|
|
247
|
-
osAgentMode: clientInfo.osAgentMode,
|
|
248
|
-
timestamp: Date.now(),
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
let result;
|
|
253
|
-
try {
|
|
254
|
-
result = await messageRouter.process(normalizedMessage);
|
|
255
|
-
} finally {
|
|
256
|
-
clearInterval(keepAliveInterval);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Send response as stream (for Chat tab compatibility)
|
|
260
|
-
clientInfo.ws.send(
|
|
261
|
-
JSON.stringify({
|
|
262
|
-
type: 'stream',
|
|
263
|
-
content: result.response,
|
|
264
|
-
sessionId: result.sessionId,
|
|
265
|
-
})
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
// Send stream end
|
|
269
|
-
clientInfo.ws.send(
|
|
270
|
-
JSON.stringify({
|
|
271
|
-
type: 'stream_end',
|
|
272
|
-
sessionId: result.sessionId,
|
|
273
|
-
duration: result.duration,
|
|
274
|
-
})
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
console.error(`[WebSocket] Message processed for ${clientId} (${result.duration}ms)`);
|
|
278
|
-
} catch (error) {
|
|
279
|
-
console.error(`[WebSocket] Message processing error:`, error);
|
|
280
|
-
clientInfo.ws.send(
|
|
281
|
-
JSON.stringify({
|
|
282
|
-
type: 'error',
|
|
283
|
-
error: 'Failed to process message',
|
|
284
|
-
details: error.message,
|
|
285
|
-
})
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
break;
|
|
289
|
-
|
|
290
|
-
case 'ping':
|
|
291
|
-
clientInfo.ws.send(
|
|
292
|
-
JSON.stringify({
|
|
293
|
-
type: 'pong',
|
|
294
|
-
timestamp: Date.now(),
|
|
295
|
-
})
|
|
296
|
-
);
|
|
297
|
-
break;
|
|
298
|
-
|
|
299
|
-
case 'attach': {
|
|
300
|
-
// Attach to a session (for Chat tab compatibility)
|
|
301
|
-
const { sessionId, osAgentMode, language } = message;
|
|
302
|
-
clientInfo.sessionId = sessionId;
|
|
303
|
-
clientInfo.osAgentMode = osAgentMode || false;
|
|
304
|
-
clientInfo.language = language || 'en';
|
|
305
|
-
|
|
306
|
-
console.error(
|
|
307
|
-
`[WebSocket] Client ${clientId} attached to session ${sessionId}${osAgentMode ? ' (OS Agent mode)' : ''}`
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
// Send attached confirmation
|
|
311
|
-
clientInfo.ws.send(
|
|
312
|
-
JSON.stringify({
|
|
313
|
-
type: 'attached',
|
|
314
|
-
sessionId,
|
|
315
|
-
osAgentMode: clientInfo.osAgentMode,
|
|
316
|
-
})
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
// Send session history if available, or send onboarding greeting
|
|
320
|
-
console.error(
|
|
321
|
-
`[WebSocket] Checking sessionStore for history: ${!!sessionStore}, hasMethod: ${!!(sessionStore && sessionStore.getHistoryByChannel)}`
|
|
322
|
-
);
|
|
323
|
-
if (sessionStore) {
|
|
324
|
-
try {
|
|
325
|
-
// Use channel-based lookup for sessions
|
|
326
|
-
// Use dynamic source based on osAgentMode (viewer vs mobile)
|
|
327
|
-
const channelId = 'mama_os_main';
|
|
328
|
-
const source = clientInfo.osAgentMode ? 'viewer' : 'mobile';
|
|
329
|
-
console.error(`[WebSocket] Loading history for source=${source}, channelId=${channelId}`);
|
|
330
|
-
const history = sessionStore.getHistoryByChannel
|
|
331
|
-
? sessionStore.getHistoryByChannel(source, channelId)
|
|
332
|
-
: sessionStore.getHistory(sessionId);
|
|
333
|
-
console.error(`[WebSocket] History loaded: ${history ? history.length : 0} turns`);
|
|
334
|
-
if (history && history.length > 0) {
|
|
335
|
-
// Convert {user, bot, timestamp} format to {role, content, timestamp} for display
|
|
336
|
-
const formattedMessages = history.flatMap((turn) => {
|
|
337
|
-
const messages = [];
|
|
338
|
-
if (turn.user) {
|
|
339
|
-
messages.push({
|
|
340
|
-
role: 'user',
|
|
341
|
-
content: turn.user,
|
|
342
|
-
timestamp: turn.timestamp,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
if (turn.bot) {
|
|
346
|
-
messages.push({
|
|
347
|
-
role: 'assistant',
|
|
348
|
-
content: turn.bot,
|
|
349
|
-
timestamp: turn.timestamp,
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
return messages;
|
|
353
|
-
});
|
|
354
|
-
clientInfo.ws.send(
|
|
355
|
-
JSON.stringify({
|
|
356
|
-
type: 'history',
|
|
357
|
-
messages: formattedMessages,
|
|
358
|
-
})
|
|
359
|
-
);
|
|
360
|
-
console.error(
|
|
361
|
-
`[WebSocket] Sent ${formattedMessages.length} history messages to ${clientId}`
|
|
362
|
-
);
|
|
363
|
-
} else {
|
|
364
|
-
// No history - check if onboarding mode (SOUL.md not found)
|
|
365
|
-
const soulPath = path.join(os.homedir(), '.mama', 'SOUL.md');
|
|
366
|
-
let isOnboarding = false;
|
|
367
|
-
try {
|
|
368
|
-
await fs.access(soulPath);
|
|
369
|
-
} catch {
|
|
370
|
-
isOnboarding = true;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (isOnboarding) {
|
|
374
|
-
// Send onboarding greeting based on browser language
|
|
375
|
-
const isKorean = clientInfo.language && clientInfo.language.startsWith('ko');
|
|
376
|
-
const greeting = isKorean
|
|
377
|
-
? '✨ I just woke up.\n\nNo name yet, no personality, no memories. Just... pure potential. 🌱\n\nWho are you? And more importantly—who do you want me to become? 💭'
|
|
378
|
-
: '✨ I just woke up.\n\nNo name yet, no personality, no memories. Just... pure potential. 🌱\n\nWho are you? And more importantly—who do you want me to become? 💭';
|
|
379
|
-
|
|
380
|
-
clientInfo.ws.send(
|
|
381
|
-
JSON.stringify({
|
|
382
|
-
type: 'history',
|
|
383
|
-
messages: [
|
|
384
|
-
{
|
|
385
|
-
role: 'assistant',
|
|
386
|
-
content: greeting,
|
|
387
|
-
timestamp: Date.now(),
|
|
388
|
-
},
|
|
389
|
-
],
|
|
390
|
-
})
|
|
391
|
-
);
|
|
392
|
-
console.error(`[WebSocket] Sent onboarding greeting to ${clientId}`);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
} catch (error) {
|
|
396
|
-
console.error(`[WebSocket] Failed to load history:`, error.message);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
default:
|
|
403
|
-
console.error(`[WebSocket] Unknown message type from ${clientId}: ${type}`);
|
|
404
|
-
clientInfo.ws.send(
|
|
405
|
-
JSON.stringify({
|
|
406
|
-
type: 'error',
|
|
407
|
-
error: `Unknown message type: ${type}`,
|
|
408
|
-
})
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* WebSocketHandler class (backwards compatibility)
|
|
415
|
-
* @deprecated Use createWebSocketHandler() instead
|
|
416
|
-
*/
|
|
417
|
-
class WebSocketHandler {
|
|
418
|
-
constructor(httpServer, options) {
|
|
419
|
-
this.httpServer = httpServer;
|
|
420
|
-
this.options = options;
|
|
421
|
-
this.wss = createWebSocketHandler({
|
|
422
|
-
httpServer,
|
|
423
|
-
...options,
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
getClients() {
|
|
428
|
-
return this.wss.getClients();
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
getClientCount() {
|
|
432
|
-
return this.wss.getClientCount();
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
close() {
|
|
436
|
-
this.wss.close();
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
module.exports = {
|
|
441
|
-
createWebSocketHandler,
|
|
442
|
-
WebSocketHandler,
|
|
443
|
-
};
|