xiaozuoassistant 0.1.69 → 0.1.70

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.
@@ -14,8 +14,11 @@ function normalizeWorkspace(input) {
14
14
  }
15
15
  export class ShortTermMemory {
16
16
  constructor() {
17
- this.metaCache = new Map();
17
+ // 主存:所有 Session Meta 数据
18
+ this.sessions = new Map();
18
19
  this.initPromise = null;
20
+ // 持久化队列,确保串行写入 index.json
21
+ this.saveQueue = Promise.resolve();
19
22
  fs.ensureDirSync(SESSIONS_DIR);
20
23
  }
21
24
  static getInstance() {
@@ -31,18 +34,61 @@ export class ShortTermMemory {
31
34
  await this.initPromise;
32
35
  }
33
36
  async init() {
34
- fs.ensureDirSync(SESSIONS_DIR);
35
- const indexExists = await fs.pathExists(INDEX_FILE);
36
- if (indexExists) {
37
- await this.warmMetaCache();
38
- return;
37
+ try {
38
+ await fs.ensureDir(SESSIONS_DIR);
39
+ const indexExists = await fs.pathExists(INDEX_FILE);
40
+ if (indexExists) {
41
+ await this.loadSessionsFromDisk();
42
+ }
43
+ else {
44
+ await this.migrateLegacyIfNeeded();
45
+ // 迁移后再次尝试加载,如果还没有则初始化为空
46
+ if (await fs.pathExists(INDEX_FILE)) {
47
+ await this.loadSessionsFromDisk();
48
+ }
49
+ else {
50
+ this.sessions.clear();
51
+ await this.persistIndex();
52
+ }
53
+ }
54
+ console.log(`[ShortTermMemory] Initialized with ${this.sessions.size} sessions.`);
55
+ }
56
+ catch (e) {
57
+ console.error('[ShortTermMemory] Initialization failed:', e);
58
+ // 即使失败,sessions 也是空的 Map,不会崩溃,但数据可能丢失
59
+ }
60
+ }
61
+ async loadSessionsFromDisk() {
62
+ try {
63
+ const data = await fs.readJson(INDEX_FILE);
64
+ this.sessions.clear();
65
+ if (Array.isArray(data)) {
66
+ for (const meta of data) {
67
+ if (meta && typeof meta.id === 'string') {
68
+ this.sessions.set(meta.id, meta);
69
+ }
70
+ }
71
+ }
39
72
  }
40
- await this.migrateLegacyIfNeeded();
41
- const existsAfter = await fs.pathExists(INDEX_FILE);
42
- if (!existsAfter) {
43
- await fs.writeJson(INDEX_FILE, [], { spaces: 2 });
73
+ catch (e) {
74
+ console.error('[ShortTermMemory] Failed to load index.json:', e);
75
+ // 此时保持 sessions 为空或之前的状态
44
76
  }
45
- await this.warmMetaCache();
77
+ }
78
+ async persistIndex() {
79
+ // 将当前所有 sessions 转为数组并排序
80
+ const metas = Array.from(this.sessions.values());
81
+ metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
82
+ // 加入写入队列
83
+ this.saveQueue = this.saveQueue.then(async () => {
84
+ try {
85
+ await fs.writeJson(INDEX_FILE, metas, { spaces: 2 });
86
+ }
87
+ catch (e) {
88
+ console.error('[ShortTermMemory] Failed to persist index.json:', e);
89
+ }
90
+ });
91
+ return this.saveQueue;
46
92
  }
47
93
  getSessionDir(sessionId) {
48
94
  return path.join(SESSIONS_DIR, sessionId);
@@ -56,34 +102,12 @@ export class ShortTermMemory {
56
102
  getRunsFile(sessionId) {
57
103
  return path.join(this.getSessionDir(sessionId), 'runs.json');
58
104
  }
59
- async loadIndex() {
60
- try {
61
- const data = await fs.readJson(INDEX_FILE);
62
- if (Array.isArray(data))
63
- return data;
64
- return [];
65
- }
66
- catch {
67
- return [];
68
- }
69
- }
70
- async saveIndex(metas) {
71
- await fs.writeJson(INDEX_FILE, metas, { spaces: 2 });
72
- }
73
- async warmMetaCache() {
74
- const metas = await this.loadIndex();
75
- this.metaCache.clear();
76
- for (const meta of metas) {
77
- if (meta && typeof meta.id === 'string') {
78
- this.metaCache.set(meta.id, meta);
79
- }
80
- }
81
- }
82
105
  async migrateLegacyIfNeeded() {
83
106
  const files = await fs.readdir(SESSIONS_DIR);
84
107
  const legacyFiles = files.filter(f => f.endsWith('.json') && f !== 'index.json');
85
108
  if (legacyFiles.length === 0)
86
109
  return;
110
+ console.log('[ShortTermMemory] Migrating legacy sessions...');
87
111
  const metas = [];
88
112
  for (const file of legacyFiles) {
89
113
  const filePath = path.join(SESSIONS_DIR, file);
@@ -113,12 +137,16 @@ export class ShortTermMemory {
113
137
  }
114
138
  if (metas.length > 0) {
115
139
  metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
116
- await fs.writeJson(INDEX_FILE, metas, { spaces: 2 });
140
+ for (const m of metas) {
141
+ this.sessions.set(m.id, m);
142
+ }
143
+ await this.persistIndex();
117
144
  }
118
145
  }
119
146
  async createSession(input) {
120
147
  await this.ensureReady();
121
148
  const id = uuidv4();
149
+ console.log(`[ShortTermMemory] Creating session: ${id}`);
122
150
  const now = Date.now();
123
151
  const meta = {
124
152
  id,
@@ -136,35 +164,32 @@ export class ShortTermMemory {
136
164
  const sessionDir = this.getSessionDir(id);
137
165
  await fs.ensureDir(sessionDir);
138
166
  await fs.writeJson(this.getMessagesFile(id), [], { spaces: 2 });
139
- const metas = await this.loadIndex();
140
- metas.unshift(meta);
141
- await this.saveIndex(metas);
142
- this.metaCache.set(id, meta);
167
+ // 更新内存
168
+ this.sessions.set(id, meta);
169
+ // 异步持久化
170
+ await this.persistIndex();
171
+ console.log(`[ShortTermMemory] Session created: ${id}`);
143
172
  return { meta, messages: [] };
144
173
  }
145
174
  async listSessions() {
146
175
  await this.ensureReady();
147
- const metas = await this.loadIndex();
176
+ const metas = Array.from(this.sessions.values());
148
177
  metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
149
178
  return metas;
150
179
  }
151
180
  async getSessionMeta(id) {
152
181
  await this.ensureReady();
153
- if (this.metaCache.has(id))
154
- return this.metaCache.get(id);
155
- const metas = await this.loadIndex();
156
- const found = metas.find(m => m.id === id) || null;
157
- if (found)
158
- this.metaCache.set(id, found);
159
- return found;
182
+ const meta = this.sessions.get(id) || null;
183
+ if (!meta) {
184
+ console.log(`[ShortTermMemory] Session not found in memory: ${id}`);
185
+ }
186
+ return meta;
160
187
  }
161
188
  async updateSessionMeta(id, patch) {
162
189
  await this.ensureReady();
163
- const metas = await this.loadIndex();
164
- const idx = metas.findIndex(m => m.id === id);
165
- if (idx < 0)
190
+ const current = this.sessions.get(id);
191
+ if (!current)
166
192
  throw new Error('Session not found');
167
- const current = metas[idx];
168
193
  const next = {
169
194
  ...current,
170
195
  alias: patch.alias === null ? undefined : (patch.alias !== undefined ? (String(patch.alias).trim() || undefined) : current.alias),
@@ -192,24 +217,17 @@ export class ShortTermMemory {
192
217
  next.context = val;
193
218
  }
194
219
  }
195
- const metaForIndex = { ...next };
196
- delete metaForIndex.context;
197
- metas[idx] = metaForIndex;
198
- metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
199
- await this.saveIndex(metas);
200
- this.metaCache.set(id, next);
220
+ // 更新内存并持久化
221
+ this.sessions.set(id, next);
222
+ await this.persistIndex();
201
223
  return next;
202
224
  }
203
225
  async persistSession(id) {
204
226
  await this.ensureReady();
205
- const current = await this.getSessionMeta(id);
227
+ const current = this.sessions.get(id);
206
228
  if (!current)
207
229
  throw new Error('Session not found');
208
230
  const now = Date.now();
209
- const metas = await this.loadIndex();
210
- const idx = metas.findIndex(m => m.id === id);
211
- if (idx < 0)
212
- throw new Error('Session not found');
213
231
  let context = '';
214
232
  try {
215
233
  const ctxFile = this.getContextFile(id);
@@ -221,10 +239,8 @@ export class ShortTermMemory {
221
239
  // ignore
222
240
  }
223
241
  const next = { ...current, context: context || undefined, updatedAt: now, lastActiveAt: now };
224
- metas[idx] = { ...metas[idx], updatedAt: now, lastActiveAt: now };
225
- metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
226
- await this.saveIndex(metas);
227
- this.metaCache.set(id, next);
242
+ this.sessions.set(id, next);
243
+ await this.persistIndex();
228
244
  await fs.ensureDir(this.getSessionDir(id));
229
245
  await fs.ensureFile(this.getMessagesFile(id));
230
246
  await fs.ensureFile(this.getRunsFile(id));
@@ -241,6 +257,7 @@ export class ShortTermMemory {
241
257
  messages = Array.isArray(data) ? data : [];
242
258
  }
243
259
  catch {
260
+ // 如果消息文件不存在或读取失败,尝试重新创建
244
261
  await fs.ensureDir(this.getSessionDir(id));
245
262
  await fs.writeJson(msgFile, [], { spaces: 2 });
246
263
  messages = [];
@@ -259,8 +276,7 @@ export class ShortTermMemory {
259
276
  }
260
277
  async createRun(sessionId, userContent, runId) {
261
278
  await this.ensureReady();
262
- const meta = await this.getSessionMeta(sessionId);
263
- if (!meta)
279
+ if (!this.sessions.has(sessionId))
264
280
  throw new Error('Session not found');
265
281
  const now = Date.now();
266
282
  const id = runId || uuidv4();
@@ -323,88 +339,91 @@ export class ShortTermMemory {
323
339
  }
324
340
  }
325
341
  async getMessages(sessionId) {
342
+ // 复用 getSession 逻辑,它包含了文件读取和错误处理
326
343
  const session = await this.getSession(sessionId);
327
344
  return session ? session.messages : [];
328
345
  }
329
346
  async addMessage(sessionId, message) {
330
347
  await this.ensureReady();
331
- const session = await this.getSession(sessionId);
332
- if (!session)
348
+ const sessionMeta = this.sessions.get(sessionId);
349
+ if (!sessionMeta)
333
350
  throw new Error(`Session ${sessionId} not found`);
334
351
  const msg = { ...message };
335
352
  if (!msg.timestamp)
336
353
  msg.timestamp = Date.now();
337
- session.messages.push(msg);
338
- await fs.writeJson(this.getMessagesFile(sessionId), session.messages, { spaces: 2 });
339
- const metas = await this.loadIndex();
340
- const idx = metas.findIndex(m => m.id === sessionId);
341
- if (idx >= 0) {
342
- metas[idx] = {
343
- ...metas[idx],
344
- updatedAt: Date.now(),
345
- lastActiveAt: Date.now()
346
- };
347
- metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
348
- await this.saveIndex(metas);
349
- this.metaCache.set(sessionId, metas.find(m => m.id === sessionId));
354
+ // 更新 messages.json
355
+ // 注意:这里仍然存在 messages.json 的并发写入风险,但比 index.json 低,因为通常一个 session 是串行的
356
+ // 如果需要严格安全,也应该为每个 session 维护一个 write queue
357
+ const msgFile = this.getMessagesFile(sessionId);
358
+ let messages = [];
359
+ try {
360
+ const data = await fs.readJson(msgFile);
361
+ messages = Array.isArray(data) ? data : [];
362
+ }
363
+ catch {
364
+ messages = [];
350
365
  }
366
+ messages.push(msg);
367
+ await fs.writeJson(msgFile, messages, { spaces: 2 });
368
+ // 更新 Session Meta
369
+ const nextMeta = {
370
+ ...sessionMeta,
371
+ updatedAt: Date.now(),
372
+ lastActiveAt: Date.now()
373
+ };
374
+ this.sessions.set(sessionId, nextMeta);
375
+ await this.persistIndex();
351
376
  }
352
377
  async clearMessages(sessionId) {
353
378
  await this.ensureReady();
354
- const session = await this.getSession(sessionId);
355
- if (!session)
379
+ const sessionMeta = this.sessions.get(sessionId);
380
+ if (!sessionMeta)
356
381
  throw new Error(`Session ${sessionId} not found`);
357
382
  await fs.writeJson(this.getMessagesFile(sessionId), [], { spaces: 2 });
358
383
  // Update timestamp
359
- const metas = await this.loadIndex();
360
- const idx = metas.findIndex(m => m.id === sessionId);
361
- if (idx >= 0) {
362
- metas[idx] = {
363
- ...metas[idx],
364
- updatedAt: Date.now(),
365
- lastActiveAt: Date.now()
366
- };
367
- await this.saveIndex(metas);
368
- this.metaCache.set(sessionId, metas[idx]);
369
- }
384
+ const nextMeta = {
385
+ ...sessionMeta,
386
+ updatedAt: Date.now(),
387
+ lastActiveAt: Date.now()
388
+ };
389
+ this.sessions.set(sessionId, nextMeta);
390
+ await this.persistIndex();
370
391
  }
371
392
  async deleteSession(id) {
372
393
  await this.ensureReady();
373
- const metas = await this.loadIndex();
374
- const next = metas.filter(m => m.id !== id);
375
- if (next.length === metas.length)
394
+ if (!this.sessions.has(id))
376
395
  throw new Error('Session not found');
377
- await this.saveIndex(next);
378
- this.metaCache.delete(id);
379
- await fs.remove(this.getSessionDir(id));
396
+ this.sessions.delete(id);
397
+ await this.persistIndex();
398
+ try {
399
+ await fs.remove(this.getSessionDir(id));
400
+ }
401
+ catch (e) {
402
+ console.error(`[ShortTermMemory] Failed to delete session dir ${id}:`, e);
403
+ }
380
404
  }
381
405
  async cleanupOldSessions(maxAgeDays) {
382
406
  await this.ensureReady();
383
- const metas = await this.loadIndex();
384
407
  const now = Date.now();
385
408
  const msPerDay = 24 * 60 * 60 * 1000;
386
- const keep = [];
387
409
  const toDelete = [];
388
- for (const meta of metas) {
410
+ for (const meta of this.sessions.values()) {
389
411
  const updatedAt = typeof meta.updatedAt === 'number' ? meta.updatedAt : 0;
390
412
  const ageDays = (now - updatedAt) / msPerDay;
391
413
  if (ageDays > maxAgeDays)
392
- toDelete.push(meta);
393
- else
394
- keep.push(meta);
414
+ toDelete.push(meta.id);
395
415
  }
396
- for (const meta of toDelete) {
416
+ for (const id of toDelete) {
417
+ this.sessions.delete(id);
397
418
  try {
398
- await fs.remove(this.getSessionDir(meta.id));
399
- this.metaCache.delete(meta.id);
419
+ await fs.remove(this.getSessionDir(id));
400
420
  }
401
421
  catch (e) {
402
- console.error(`[ShortTermMemory] Failed to delete session ${meta.id}:`, e);
422
+ console.error(`[ShortTermMemory] Failed to delete session ${id}:`, e);
403
423
  }
404
424
  }
405
425
  if (toDelete.length > 0) {
406
- keep.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
407
- await this.saveIndex(keep);
426
+ await this.persistIndex();
408
427
  }
409
428
  return toDelete.length;
410
429
  }
@@ -547,8 +547,10 @@ eventBus.onEvent('message', async (event) => {
547
547
  const { sessionId, payload, channel: channelName } = event;
548
548
  const content = payload.content;
549
549
  try {
550
+ console.log(`[EventBus] Processing message for session: ${sessionId}`);
550
551
  const session = await memory.getSession(sessionId);
551
552
  if (!session) {
553
+ console.error(`[EventBus] Session not found: ${sessionId}`);
552
554
  throw new Error('Session not found. Please create or select a session first.');
553
555
  }
554
556
  await memory.addMessage(sessionId, { role: 'user', content, timestamp: Date.now() });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "xiaozuoassistant",
3
3
  "private": false,
4
4
  "description": "Your personal, locally-hosted AI assistant for office productivity.",
5
- "version": "0.1.69",
5
+ "version": "0.1.70",
6
6
  "author": "mantle.lau",
7
7
  "license": "MIT",
8
8
  "repository": {