R.C.M. commited on
Commit ·
2390ffd
1
Parent(s): 03d60b4
Add new session storage format
Browse files- server/sessionStore.js +133 -5
- server/wsHandler.js +31 -4
server/sessionStore.js
CHANGED
|
@@ -45,13 +45,129 @@ function safeFileId(value, fallback = 'unknown') {
|
|
| 45 |
return normalized.replace(/[^a-zA-Z0-9_.-]+/g, '_').slice(0, 160) || fallback;
|
| 46 |
}
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
function ensureSessionShape(raw, fallbackId = null) {
|
| 49 |
const created = Number.isFinite(raw?.created) ? raw.created : Date.now();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
return {
|
| 51 |
id: raw?.id || fallbackId || crypto.randomUUID(),
|
| 52 |
name: String(raw?.name || 'New Chat'),
|
| 53 |
created,
|
| 54 |
-
history
|
| 55 |
model: raw?.model || null,
|
| 56 |
};
|
| 57 |
}
|
|
@@ -197,8 +313,12 @@ async function loadTempStore() {
|
|
| 197 |
const data = await loadEncryptedJson(TEMP_STORE_FILE, 'chat:temp:index');
|
| 198 |
if (!data) return;
|
| 199 |
for (const [id, d] of Object.entries(data)) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
tempStore.set(id, {
|
| 201 |
-
sessions: new Map(Object.entries(
|
| 202 |
msgCount: d.msgCount || 0,
|
| 203 |
created: d.created || Date.now(),
|
| 204 |
lastActive: d.lastActive || Date.now(),
|
|
@@ -263,7 +383,13 @@ export const sessionStore = {
|
|
| 263 |
},
|
| 264 |
createTempSession(t) {
|
| 265 |
const d = this.initTemp(t);
|
| 266 |
-
const s =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
d.sessions.set(s.id, s);
|
| 268 |
d.lastActive = Date.now();
|
| 269 |
saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
|
@@ -275,13 +401,15 @@ export const sessionStore = {
|
|
| 275 |
const s = d.sessions.get(id);
|
| 276 |
if (!s) return null;
|
| 277 |
Object.assign(s, patch);
|
|
|
|
|
|
|
| 278 |
d.lastActive = Date.now();
|
| 279 |
saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
| 280 |
-
return
|
| 281 |
},
|
| 282 |
restoreTempSession(t, session) {
|
| 283 |
const d = this.initTemp(t);
|
| 284 |
-
const restored = JSON.parse(JSON.stringify(session));
|
| 285 |
d.sessions.set(restored.id, restored);
|
| 286 |
d.lastActive = Date.now();
|
| 287 |
saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
|
|
|
| 45 |
return normalized.replace(/[^a-zA-Z0-9_.-]+/g, '_').slice(0, 160) || fallback;
|
| 46 |
}
|
| 47 |
|
| 48 |
+
function cloneJson(value) {
|
| 49 |
+
return JSON.parse(JSON.stringify(value));
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function ensureMessageVersioningShape(message) {
|
| 53 |
+
if (!message || typeof message !== 'object') return message;
|
| 54 |
+
if (!Array.isArray(message.versions) || message.versions.length === 0) {
|
| 55 |
+
message.versions = [{
|
| 56 |
+
content: message.content ?? '',
|
| 57 |
+
tail: [],
|
| 58 |
+
timestamp: Number.isFinite(message.timestamp) ? message.timestamp : Date.now(),
|
| 59 |
+
}];
|
| 60 |
+
message.currentVersionIdx = 0;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
const idx = Number.isInteger(message.currentVersionIdx)
|
| 64 |
+
? Math.max(0, Math.min(message.currentVersionIdx, message.versions.length - 1))
|
| 65 |
+
: 0;
|
| 66 |
+
message.currentVersionIdx = idx;
|
| 67 |
+
|
| 68 |
+
const active = message.versions[idx] || {};
|
| 69 |
+
if (!Array.isArray(active.tail)) active.tail = [];
|
| 70 |
+
if (active.content === undefined || active.content === null) {
|
| 71 |
+
active.content = message.content ?? '';
|
| 72 |
+
}
|
| 73 |
+
if (!Number.isFinite(active.timestamp)) {
|
| 74 |
+
active.timestamp = Number.isFinite(message.timestamp) ? message.timestamp : Date.now();
|
| 75 |
+
}
|
| 76 |
+
message.versions[idx] = active;
|
| 77 |
+
message.content = active.content;
|
| 78 |
+
return message;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
function getActiveVersionNode(message) {
|
| 82 |
+
if (!message || typeof message !== 'object') return null;
|
| 83 |
+
ensureMessageVersioningShape(message);
|
| 84 |
+
return message.versions[message.currentVersionIdx];
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
function cloneAndRepairTree(rootMessage) {
|
| 88 |
+
if (!rootMessage || typeof rootMessage !== 'object') return null;
|
| 89 |
+
const cloned = cloneJson(rootMessage);
|
| 90 |
+
const walk = (node) => {
|
| 91 |
+
if (!node || typeof node !== 'object') return;
|
| 92 |
+
ensureMessageVersioningShape(node);
|
| 93 |
+
const active = getActiveVersionNode(node);
|
| 94 |
+
for (const child of active?.tail || []) walk(child);
|
| 95 |
+
};
|
| 96 |
+
walk(cloned);
|
| 97 |
+
return cloned;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
function getActiveLeafMessage(rootMessage) {
|
| 101 |
+
let current = rootMessage;
|
| 102 |
+
while (current) {
|
| 103 |
+
const active = getActiveVersionNode(current);
|
| 104 |
+
const tail = Array.isArray(active?.tail) ? active.tail : [];
|
| 105 |
+
if (!tail.length) return current;
|
| 106 |
+
current = tail[tail.length - 1];
|
| 107 |
+
}
|
| 108 |
+
return rootMessage;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function appendEntriesToActiveLeaf(rootMessage, entries = []) {
|
| 112 |
+
if (!rootMessage || !entries.length) return rootMessage;
|
| 113 |
+
const leaf = getActiveLeafMessage(rootMessage);
|
| 114 |
+
const active = getActiveVersionNode(leaf);
|
| 115 |
+
active.tail = [...(active.tail || []), ...entries];
|
| 116 |
+
return rootMessage;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
function normalizeHistoryToTree(history) {
|
| 120 |
+
const rawHistory = Array.isArray(history) ? history : [];
|
| 121 |
+
if (!rawHistory.length) return [];
|
| 122 |
+
|
| 123 |
+
// Already tree-shaped ([rootMessage]).
|
| 124 |
+
if (
|
| 125 |
+
rawHistory.length === 1 &&
|
| 126 |
+
rawHistory[0] &&
|
| 127 |
+
typeof rawHistory[0] === 'object' &&
|
| 128 |
+
Array.isArray(rawHistory[0].versions)
|
| 129 |
+
) {
|
| 130 |
+
const root = cloneAndRepairTree(rawHistory[0]);
|
| 131 |
+
return root ? [root] : [];
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// Legacy flat history; rebuild a linear active-branch tree.
|
| 135 |
+
const nodes = rawHistory
|
| 136 |
+
.filter((entry) => entry && typeof entry === 'object')
|
| 137 |
+
.map((entry) => ensureMessageVersioningShape(cloneJson(entry)));
|
| 138 |
+
if (!nodes.length) return [];
|
| 139 |
+
const root = nodes[0];
|
| 140 |
+
for (let i = 1; i < nodes.length; i++) {
|
| 141 |
+
appendEntriesToActiveLeaf(root, [nodes[i]]);
|
| 142 |
+
}
|
| 143 |
+
return [root];
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
function ensureSessionShape(raw, fallbackId = null) {
|
| 147 |
const created = Number.isFinite(raw?.created) ? raw.created : Date.now();
|
| 148 |
+
let history = [];
|
| 149 |
+
|
| 150 |
+
// Prefer desktop's legacy serialized tree when present.
|
| 151 |
+
const legacyRootRaw = typeof raw?.__historyRootJson === 'string'
|
| 152 |
+
? raw.__historyRootJson.trim()
|
| 153 |
+
: '';
|
| 154 |
+
if (legacyRootRaw) {
|
| 155 |
+
try {
|
| 156 |
+
const parsedRoot = cloneAndRepairTree(JSON.parse(legacyRootRaw));
|
| 157 |
+
if (parsedRoot) history = [parsedRoot];
|
| 158 |
+
} catch {
|
| 159 |
+
history = [];
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
if (!history.length) {
|
| 163 |
+
history = normalizeHistoryToTree(raw?.history);
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
return {
|
| 167 |
id: raw?.id || fallbackId || crypto.randomUUID(),
|
| 168 |
name: String(raw?.name || 'New Chat'),
|
| 169 |
created,
|
| 170 |
+
history,
|
| 171 |
model: raw?.model || null,
|
| 172 |
};
|
| 173 |
}
|
|
|
|
| 313 |
const data = await loadEncryptedJson(TEMP_STORE_FILE, 'chat:temp:index');
|
| 314 |
if (!data) return;
|
| 315 |
for (const [id, d] of Object.entries(data)) {
|
| 316 |
+
const restoredSessions = {};
|
| 317 |
+
for (const [sessionId, sessionValue] of Object.entries(d.sessions || {})) {
|
| 318 |
+
restoredSessions[sessionId] = ensureSessionShape(sessionValue, sessionId);
|
| 319 |
+
}
|
| 320 |
tempStore.set(id, {
|
| 321 |
+
sessions: new Map(Object.entries(restoredSessions)),
|
| 322 |
msgCount: d.msgCount || 0,
|
| 323 |
created: d.created || Date.now(),
|
| 324 |
lastActive: d.lastActive || Date.now(),
|
|
|
|
| 383 |
},
|
| 384 |
createTempSession(t) {
|
| 385 |
const d = this.initTemp(t);
|
| 386 |
+
const s = ensureSessionShape({
|
| 387 |
+
id: crypto.randomUUID(),
|
| 388 |
+
name: 'New Chat',
|
| 389 |
+
created: Date.now(),
|
| 390 |
+
history: [],
|
| 391 |
+
model: null,
|
| 392 |
+
});
|
| 393 |
d.sessions.set(s.id, s);
|
| 394 |
d.lastActive = Date.now();
|
| 395 |
saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
|
|
|
| 401 |
const s = d.sessions.get(id);
|
| 402 |
if (!s) return null;
|
| 403 |
Object.assign(s, patch);
|
| 404 |
+
const shaped = ensureSessionShape(s, id);
|
| 405 |
+
d.sessions.set(id, shaped);
|
| 406 |
d.lastActive = Date.now();
|
| 407 |
saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
| 408 |
+
return shaped;
|
| 409 |
},
|
| 410 |
restoreTempSession(t, session) {
|
| 411 |
const d = this.initTemp(t);
|
| 412 |
+
const restored = ensureSessionShape(JSON.parse(JSON.stringify(session)));
|
| 413 |
d.sessions.set(restored.id, restored);
|
| 414 |
d.lastActive = Date.now();
|
| 415 |
saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
server/wsHandler.js
CHANGED
|
@@ -454,7 +454,13 @@ const handlers = {
|
|
| 454 |
await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory, name: newName });
|
| 455 |
else sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory, name: newName });
|
| 456 |
|
| 457 |
-
safeSend(ws, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
},
|
| 459 |
onError(err) {
|
| 460 |
activeStreams.delete(ws);
|
|
@@ -525,7 +531,15 @@ const handlers = {
|
|
| 525 |
return safeSend(ws, { type: 'error', message: 'Failed to apply edit - message lost' });
|
| 526 |
}
|
| 527 |
|
| 528 |
-
safeSend(ws, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
},
|
| 530 |
|
| 531 |
'chat:selectVersion': async (ws, msg, client) => {
|
|
@@ -560,7 +574,14 @@ const handlers = {
|
|
| 560 |
}
|
| 561 |
|
| 562 |
// Send back with messageId for clarity
|
| 563 |
-
safeSend(ws, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
},
|
| 565 |
|
| 566 |
'chat:assistantAction': async (ws, msg, client) => {
|
|
@@ -709,7 +730,13 @@ const handlers = {
|
|
| 709 |
sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory, name: newName });
|
| 710 |
}
|
| 711 |
|
| 712 |
-
safeSend(ws, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
},
|
| 714 |
onError(err) {
|
| 715 |
activeStreams.delete(ws);
|
|
|
|
| 454 |
await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory, name: newName });
|
| 455 |
else sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory, name: newName });
|
| 456 |
|
| 457 |
+
safeSend(ws, {
|
| 458 |
+
type: aborted ? 'chat:aborted' : 'chat:done',
|
| 459 |
+
sessionId,
|
| 460 |
+
name: newName,
|
| 461 |
+
history: newHistory,
|
| 462 |
+
flatHistory: extractFlatHistory(newRootMessage),
|
| 463 |
+
});
|
| 464 |
},
|
| 465 |
onError(err) {
|
| 466 |
activeStreams.delete(ws);
|
|
|
|
| 531 |
return safeSend(ws, { type: 'error', message: 'Failed to apply edit - message lost' });
|
| 532 |
}
|
| 533 |
|
| 534 |
+
safeSend(ws, {
|
| 535 |
+
type: 'chat:messageEdited',
|
| 536 |
+
sessionId,
|
| 537 |
+
messageId: targetMsg.id,
|
| 538 |
+
messageIndex,
|
| 539 |
+
message: updatedTargetMsg,
|
| 540 |
+
history: [newRoot],
|
| 541 |
+
flatHistory: updatedFlatHistory,
|
| 542 |
+
});
|
| 543 |
},
|
| 544 |
|
| 545 |
'chat:selectVersion': async (ws, msg, client) => {
|
|
|
|
| 574 |
}
|
| 575 |
|
| 576 |
// Send back with messageId for clarity
|
| 577 |
+
safeSend(ws, {
|
| 578 |
+
type: 'chat:versionSelected',
|
| 579 |
+
sessionId,
|
| 580 |
+
messageId: targetMsg.id,
|
| 581 |
+
messageIndex,
|
| 582 |
+
history: [newRoot],
|
| 583 |
+
flatHistory: extractFlatHistory(newRoot),
|
| 584 |
+
});
|
| 585 |
},
|
| 586 |
|
| 587 |
'chat:assistantAction': async (ws, msg, client) => {
|
|
|
|
| 730 |
sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory, name: newName });
|
| 731 |
}
|
| 732 |
|
| 733 |
+
safeSend(ws, {
|
| 734 |
+
type: 'chat:done',
|
| 735 |
+
sessionId,
|
| 736 |
+
name: newName,
|
| 737 |
+
history: newHistory,
|
| 738 |
+
flatHistory: extractFlatHistory(newRoot),
|
| 739 |
+
});
|
| 740 |
},
|
| 741 |
onError(err) {
|
| 742 |
activeStreams.delete(ws);
|