incognitolm commited on
Commit ·
6cf0e88
1
Parent(s): 1baccaa
Update wsHandler.js
Browse files- server/wsHandler.js +103 -20
server/wsHandler.js
CHANGED
|
@@ -12,6 +12,27 @@ import {
|
|
| 12 |
import { streamChat, extractSessionName } from './chatStream.js';
|
| 13 |
import crypto from 'crypto';
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
const activeStreams = new Map();
|
| 16 |
|
| 17 |
initGuestRequestLimiter().catch(err => console.error('Failed to initialize guest request limiter:', err));
|
|
@@ -171,7 +192,7 @@ const handlers = {
|
|
| 171 |
const assetsCollected = [], toolCallsCollected = [];
|
| 172 |
|
| 173 |
await streamChat(ws, {
|
| 174 |
-
history: session.history || [], userMessage: content, tools: tools || {},
|
| 175 |
accessToken: client.accessToken, clientId: msg.clientId, abortSignal: abort.signal,
|
| 176 |
onToken(t) { fullText += t; safeSend(ws, { type: 'chat:token', token: t, sessionId }); },
|
| 177 |
onToolCall(call) {
|
|
@@ -190,14 +211,17 @@ const handlers = {
|
|
| 190 |
return { ...c, state: resolved.state || 'resolved', result: resolved.result };
|
| 191 |
});
|
| 192 |
const asstEntry = buildEntry('assistant', finalText, mergedCalls);
|
| 193 |
-
|
| 194 |
|
| 195 |
-
// Update
|
| 196 |
-
for (let i = 0; i < newHistory.length; i++) {
|
| 197 |
const msg = newHistory[i];
|
|
|
|
| 198 |
if (msg.versions) {
|
| 199 |
for (const version of msg.versions) {
|
| 200 |
-
|
|
|
|
|
|
|
| 201 |
}
|
| 202 |
}
|
| 203 |
}
|
|
@@ -228,15 +252,24 @@ const handlers = {
|
|
| 228 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 229 |
: sessionStore.getTempSession(client.tempId, sessionId);
|
| 230 |
if (!session) return;
|
| 231 |
-
const history = session.history || [];
|
| 232 |
-
const m = history[messageIndex];
|
| 233 |
-
if (!m
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
m.currentVersionIdx = m.versions.length - 1;
|
| 236 |
m.content = newContent;
|
| 237 |
const newHistory = history.slice(0, messageIndex + 1);
|
| 238 |
-
if (client.userId)
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
| 240 |
safeSend(ws, { type: 'chat:messageEdited', sessionId, messageIndex, message: m, history: newHistory });
|
| 241 |
},
|
| 242 |
|
|
@@ -246,14 +279,17 @@ const handlers = {
|
|
| 246 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 247 |
: sessionStore.getTempSession(client.tempId, sessionId);
|
| 248 |
if (!session) return;
|
| 249 |
-
const history = session.history || [];
|
| 250 |
const m = history[messageIndex];
|
| 251 |
if (!m?.versions || versionIdx < 0 || versionIdx >= m.versions.length) return;
|
| 252 |
-
|
| 253 |
-
m.
|
| 254 |
-
const newHistory =
|
| 255 |
-
if (client.userId)
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
| 257 |
safeSend(ws, { type: 'chat:versionSelected', sessionId, history: newHistory });
|
| 258 |
},
|
| 259 |
|
|
@@ -288,6 +324,10 @@ const handlers = {
|
|
| 288 |
|
| 289 |
function ser(s) { return { id: s.id, name: s.name, created: s.created, history: s.history || [], model: s.model }; }
|
| 290 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
function buildEntry(role, content, toolCalls = []) {
|
| 292 |
const normalizedCalls = toolCalls.map(c => ({
|
| 293 |
id: c.id,
|
|
@@ -296,7 +336,50 @@ function buildEntry(role, content, toolCalls = []) {
|
|
| 296 |
state: c.state || 'resolved',
|
| 297 |
result: c.result,
|
| 298 |
}));
|
| 299 |
-
return {
|
| 300 |
-
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
}
|
|
|
|
| 12 |
import { streamChat, extractSessionName } from './chatStream.js';
|
| 13 |
import crypto from 'crypto';
|
| 14 |
|
| 15 |
+
/**
|
| 16 |
+
* ─ Message Structure Documentation ─
|
| 17 |
+
*
|
| 18 |
+
* Each message in session.history contains:
|
| 19 |
+
* {
|
| 20 |
+
* id: "msg-123...", // Unique identifier for this message
|
| 21 |
+
* role: "user" | "assistant",
|
| 22 |
+
* content: string | array,
|
| 23 |
+
* timestamp: number,
|
| 24 |
+
* versions: [
|
| 25 |
+
* {
|
| 26 |
+
* content: string | array,
|
| 27 |
+
* nextMessageId: "msg-456..." | null, // ID of next message when this version is active
|
| 28 |
+
* timestamp: number
|
| 29 |
+
* },
|
| 30 |
+
* ...
|
| 31 |
+
* ],
|
| 32 |
+
* currentVersionIdx: 0, // Which version is currently active
|
| 33 |
+
* toolCalls?: [...], // For assistant messages with tool calls
|
| 34 |
+
* }\n *\n * VERSION HISTORY: Changed from storing full message arrays in \"tail\" field\n * to storing only nextMessageId references. This reduces payload size significantly\n * while maintaining backward compatibility through normalizeHistoryStructure().\n *\n * HISTORY RECONSTRUCTION: When switching versions, the message chain is rebuilt\n * by following nextMessageId pointers from the selected version.\n */
|
| 35 |
+
|
| 36 |
const activeStreams = new Map();
|
| 37 |
|
| 38 |
initGuestRequestLimiter().catch(err => console.error('Failed to initialize guest request limiter:', err));
|
|
|
|
| 192 |
const assetsCollected = [], toolCallsCollected = [];
|
| 193 |
|
| 194 |
await streamChat(ws, {
|
| 195 |
+
history: normalizeHistoryStructure(session.history || []), userMessage: content, tools: tools || {},
|
| 196 |
accessToken: client.accessToken, clientId: msg.clientId, abortSignal: abort.signal,
|
| 197 |
onToken(t) { fullText += t; safeSend(ws, { type: 'chat:token', token: t, sessionId }); },
|
| 198 |
onToolCall(call) {
|
|
|
|
| 211 |
return { ...c, state: resolved.state || 'resolved', result: resolved.result };
|
| 212 |
});
|
| 213 |
const asstEntry = buildEntry('assistant', finalText, mergedCalls);
|
| 214 |
+
let newHistory = normalizeHistoryStructure([...(session.history || []), userEntry, asstEntry]);
|
| 215 |
|
| 216 |
+
// Update nextMessageId pointers for all versioned messages to link to the next message
|
| 217 |
+
for (let i = 0; i < newHistory.length - 1; i++) {
|
| 218 |
const msg = newHistory[i];
|
| 219 |
+
const nextMsg = newHistory[i + 1];
|
| 220 |
if (msg.versions) {
|
| 221 |
for (const version of msg.versions) {
|
| 222 |
+
if (!version.nextMessageId) {
|
| 223 |
+
version.nextMessageId = nextMsg.id;
|
| 224 |
+
}
|
| 225 |
}
|
| 226 |
}
|
| 227 |
}
|
|
|
|
| 252 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 253 |
: sessionStore.getTempSession(client.tempId, sessionId);
|
| 254 |
if (!session) return;
|
| 255 |
+
const history = normalizeHistoryStructure(session.history || []);
|
| 256 |
+
const m = history[messageIndex];
|
| 257 |
+
if (!m) return;
|
| 258 |
+
const currentVersionIdx = m.currentVersionIdx ?? 0;
|
| 259 |
+
const currentVersion = m.versions?.[currentVersionIdx];
|
| 260 |
+
const nextMessageId = currentVersion?.nextMessageId || (history[messageIndex + 1]?.id || null);
|
| 261 |
+
if (!m.versions) {
|
| 262 |
+
m.versions = [{ content: m.content, nextMessageId: nextMessageId, timestamp: m.timestamp || Date.now() }];
|
| 263 |
+
}
|
| 264 |
+
m.versions.push({ content: newContent, nextMessageId: nextMessageId, timestamp: Date.now() });
|
| 265 |
m.currentVersionIdx = m.versions.length - 1;
|
| 266 |
m.content = newContent;
|
| 267 |
const newHistory = history.slice(0, messageIndex + 1);
|
| 268 |
+
if (client.userId) {
|
| 269 |
+
await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory });
|
| 270 |
+
} else {
|
| 271 |
+
sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory });
|
| 272 |
+
}
|
| 273 |
safeSend(ws, { type: 'chat:messageEdited', sessionId, messageIndex, message: m, history: newHistory });
|
| 274 |
},
|
| 275 |
|
|
|
|
| 279 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 280 |
: sessionStore.getTempSession(client.tempId, sessionId);
|
| 281 |
if (!session) return;
|
| 282 |
+
const history = normalizeHistoryStructure(session.history || []);
|
| 283 |
const m = history[messageIndex];
|
| 284 |
if (!m?.versions || versionIdx < 0 || versionIdx >= m.versions.length) return;
|
| 285 |
+
m.currentVersionIdx = versionIdx;
|
| 286 |
+
m.content = m.versions[versionIdx].content;
|
| 287 |
+
const newHistory = reconstructHistoryFromVersion(history, messageIndex, versionIdx);
|
| 288 |
+
if (client.userId) {
|
| 289 |
+
await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory });
|
| 290 |
+
} else {
|
| 291 |
+
sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory });
|
| 292 |
+
}
|
| 293 |
safeSend(ws, { type: 'chat:versionSelected', sessionId, history: newHistory });
|
| 294 |
},
|
| 295 |
|
|
|
|
| 324 |
|
| 325 |
function ser(s) { return { id: s.id, name: s.name, created: s.created, history: s.history || [], model: s.model }; }
|
| 326 |
|
| 327 |
+
function generateMessageId() {
|
| 328 |
+
return `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
function buildEntry(role, content, toolCalls = []) {
|
| 332 |
const normalizedCalls = toolCalls.map(c => ({
|
| 333 |
id: c.id,
|
|
|
|
| 336 |
state: c.state || 'resolved',
|
| 337 |
result: c.result,
|
| 338 |
}));
|
| 339 |
+
return {
|
| 340 |
+
id: generateMessageId(),
|
| 341 |
+
role,
|
| 342 |
+
content,
|
| 343 |
+
timestamp: Date.now(),
|
| 344 |
+
versions: [{ content, nextMessageId: null, timestamp: Date.now() }],
|
| 345 |
+
currentVersionIdx: 0,
|
| 346 |
+
...(normalizedCalls.length ? { toolCalls: normalizedCalls } : {})
|
| 347 |
+
};
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
function normalizeHistoryStructure(history) {
|
| 351 |
+
if (!history || !Array.isArray(history)) return [];
|
| 352 |
+
const normalized = history.map((msg, idx) => {
|
| 353 |
+
if (!msg.id) msg.id = `msg-${msg.timestamp || Date.now()}-${idx}`;
|
| 354 |
+
if (msg.versions && Array.isArray(msg.versions)) {
|
| 355 |
+
msg.versions = msg.versions.map(v => {
|
| 356 |
+
if (v.tail !== undefined && !v.nextMessageId) {
|
| 357 |
+
v.nextMessageId = v.tail && v.tail.length > 0 ? v.tail[0].id : null;
|
| 358 |
+
}
|
| 359 |
+
if (!v.nextMessageId) v.nextMessageId = null;
|
| 360 |
+
return v;
|
| 361 |
+
});
|
| 362 |
+
}
|
| 363 |
+
return msg;
|
| 364 |
+
});
|
| 365 |
+
return normalized;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
function reconstructHistoryFromVersion(history, messageIndex, versionIdx) {
|
| 369 |
+
const baseHistory = history.slice(0, messageIndex + 1);
|
| 370 |
+
const branchMessage = baseHistory[messageIndex];
|
| 371 |
+
if (!branchMessage?.versions || versionIdx >= branchMessage.versions.length) {
|
| 372 |
+
return baseHistory;
|
| 373 |
+
}
|
| 374 |
+
const version = branchMessage.versions[versionIdx];
|
| 375 |
+
if (!version.nextMessageId) return baseHistory;
|
| 376 |
+
const reconstructed = [...baseHistory];
|
| 377 |
+
let currentId = version.nextMessageId;
|
| 378 |
+
while (currentId) {
|
| 379 |
+
const nextMsg = history.find(m => m.id === currentId);
|
| 380 |
+
if (!nextMsg) break;
|
| 381 |
+
reconstructed.push(nextMsg);
|
| 382 |
+
currentId = nextMsg.versions?.[nextMsg.currentVersionIdx]?.nextMessageId || null;
|
| 383 |
+
}
|
| 384 |
+
return reconstructed;
|
| 385 |
}
|