R.C.M. commited on
Commit
2390ffd
·
1 Parent(s): 03d60b4

Add new session storage format

Browse files
Files changed (2) hide show
  1. server/sessionStore.js +133 -5
  2. 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: Array.isArray(raw?.history) ? raw.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(d.sessions || {})),
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 = { id: crypto.randomUUID(), name: 'New Chat', created: Date.now(), history: [] };
 
 
 
 
 
 
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 s;
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, { type: aborted ? 'chat:aborted' : 'chat:done', sessionId, name: newName, history: extractFlatHistory(newRootMessage) });
 
 
 
 
 
 
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, { type: 'chat:messageEdited', sessionId, messageId: targetMsg.id, messageIndex, message: updatedTargetMsg, history: updatedFlatHistory });
 
 
 
 
 
 
 
 
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, { type: 'chat:versionSelected', sessionId, messageId: targetMsg.id, messageIndex, history: extractFlatHistory(newRoot) });
 
 
 
 
 
 
 
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, { type: 'chat:done', sessionId, name: newName, history: extractFlatHistory(newRoot) });
 
 
 
 
 
 
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);