xiaozuoassistant 0.1.69 → 0.1.71

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,13 @@ function normalizeWorkspace(input) {
14
14
  }
15
15
  export class ShortTermMemory {
16
16
  constructor() {
17
- this.metaCache = new Map();
17
+ this.instanceId = Math.random().toString(36).substring(7);
18
+ // 主存:所有 Session Meta 数据
19
+ this.sessions = new Map();
18
20
  this.initPromise = null;
21
+ // 持久化队列,确保串行写入 index.json
22
+ this.saveQueue = Promise.resolve();
23
+ console.log(`[ShortTermMemory] Instance created: ${this.instanceId}`);
19
24
  fs.ensureDirSync(SESSIONS_DIR);
20
25
  }
21
26
  static getInstance() {
@@ -31,18 +36,61 @@ export class ShortTermMemory {
31
36
  await this.initPromise;
32
37
  }
33
38
  async init() {
34
- fs.ensureDirSync(SESSIONS_DIR);
35
- const indexExists = await fs.pathExists(INDEX_FILE);
36
- if (indexExists) {
37
- await this.warmMetaCache();
38
- return;
39
+ try {
40
+ await fs.ensureDir(SESSIONS_DIR);
41
+ const indexExists = await fs.pathExists(INDEX_FILE);
42
+ if (indexExists) {
43
+ await this.loadSessionsFromDisk();
44
+ }
45
+ else {
46
+ await this.migrateLegacyIfNeeded();
47
+ // 迁移后再次尝试加载,如果还没有则初始化为空
48
+ if (await fs.pathExists(INDEX_FILE)) {
49
+ await this.loadSessionsFromDisk();
50
+ }
51
+ else {
52
+ this.sessions.clear();
53
+ await this.persistIndex();
54
+ }
55
+ }
56
+ console.log(`[ShortTermMemory] Initialized with ${this.sessions.size} sessions.`);
57
+ }
58
+ catch (e) {
59
+ console.error('[ShortTermMemory] Initialization failed:', e);
60
+ // 即使失败,sessions 也是空的 Map,不会崩溃,但数据可能丢失
61
+ }
62
+ }
63
+ async loadSessionsFromDisk() {
64
+ try {
65
+ const data = await fs.readJson(INDEX_FILE);
66
+ this.sessions.clear();
67
+ if (Array.isArray(data)) {
68
+ for (const meta of data) {
69
+ if (meta && typeof meta.id === 'string') {
70
+ this.sessions.set(meta.id, meta);
71
+ }
72
+ }
73
+ }
39
74
  }
40
- await this.migrateLegacyIfNeeded();
41
- const existsAfter = await fs.pathExists(INDEX_FILE);
42
- if (!existsAfter) {
43
- await fs.writeJson(INDEX_FILE, [], { spaces: 2 });
75
+ catch (e) {
76
+ console.error('[ShortTermMemory] Failed to load index.json:', e);
77
+ // 此时保持 sessions 为空或之前的状态
44
78
  }
45
- await this.warmMetaCache();
79
+ }
80
+ async persistIndex() {
81
+ // 将当前所有 sessions 转为数组并排序
82
+ const metas = Array.from(this.sessions.values());
83
+ metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
84
+ // 加入写入队列
85
+ this.saveQueue = this.saveQueue.then(async () => {
86
+ try {
87
+ await fs.writeJson(INDEX_FILE, metas, { spaces: 2 });
88
+ }
89
+ catch (e) {
90
+ console.error('[ShortTermMemory] Failed to persist index.json:', e);
91
+ }
92
+ });
93
+ return this.saveQueue;
46
94
  }
47
95
  getSessionDir(sessionId) {
48
96
  return path.join(SESSIONS_DIR, sessionId);
@@ -56,34 +104,12 @@ export class ShortTermMemory {
56
104
  getRunsFile(sessionId) {
57
105
  return path.join(this.getSessionDir(sessionId), 'runs.json');
58
106
  }
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
107
  async migrateLegacyIfNeeded() {
83
108
  const files = await fs.readdir(SESSIONS_DIR);
84
109
  const legacyFiles = files.filter(f => f.endsWith('.json') && f !== 'index.json');
85
110
  if (legacyFiles.length === 0)
86
111
  return;
112
+ console.log('[ShortTermMemory] Migrating legacy sessions...');
87
113
  const metas = [];
88
114
  for (const file of legacyFiles) {
89
115
  const filePath = path.join(SESSIONS_DIR, file);
@@ -113,12 +139,16 @@ export class ShortTermMemory {
113
139
  }
114
140
  if (metas.length > 0) {
115
141
  metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
116
- await fs.writeJson(INDEX_FILE, metas, { spaces: 2 });
142
+ for (const m of metas) {
143
+ this.sessions.set(m.id, m);
144
+ }
145
+ await this.persistIndex();
117
146
  }
118
147
  }
119
148
  async createSession(input) {
120
149
  await this.ensureReady();
121
150
  const id = uuidv4();
151
+ console.log(`[ShortTermMemory:${this.instanceId}] Creating session: ${id}`);
122
152
  const now = Date.now();
123
153
  const meta = {
124
154
  id,
@@ -136,35 +166,40 @@ export class ShortTermMemory {
136
166
  const sessionDir = this.getSessionDir(id);
137
167
  await fs.ensureDir(sessionDir);
138
168
  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);
169
+ // 更新内存
170
+ this.sessions.set(id, meta);
171
+ // 异步持久化
172
+ await this.persistIndex();
173
+ console.log(`[ShortTermMemory] Session created: ${id}`);
143
174
  return { meta, messages: [] };
144
175
  }
145
176
  async listSessions() {
146
177
  await this.ensureReady();
147
- const metas = await this.loadIndex();
178
+ const metas = Array.from(this.sessions.values());
148
179
  metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
149
180
  return metas;
150
181
  }
151
182
  async getSessionMeta(id) {
152
183
  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;
184
+ let meta = this.sessions.get(id) || null;
185
+ // 如果内存中没有,尝试重新加载磁盘(可能是其他进程创建的)
186
+ if (!meta) {
187
+ console.log(`[ShortTermMemory:${this.instanceId}] Cache miss for ${id}, reloading from disk...`);
188
+ await this.loadSessionsFromDisk();
189
+ meta = this.sessions.get(id) || null;
190
+ }
191
+ if (!meta) {
192
+ console.log(`[ShortTermMemory:${this.instanceId}] Session not found in memory after reload: ${id}. Total sessions: ${this.sessions.size}`);
193
+ // Log all available IDs for debugging
194
+ // console.log(`[ShortTermMemory:${this.instanceId}] Available IDs: ${Array.from(this.sessions.keys()).join(', ')}`);
195
+ }
196
+ return meta;
160
197
  }
161
198
  async updateSessionMeta(id, patch) {
162
199
  await this.ensureReady();
163
- const metas = await this.loadIndex();
164
- const idx = metas.findIndex(m => m.id === id);
165
- if (idx < 0)
200
+ const current = this.sessions.get(id);
201
+ if (!current)
166
202
  throw new Error('Session not found');
167
- const current = metas[idx];
168
203
  const next = {
169
204
  ...current,
170
205
  alias: patch.alias === null ? undefined : (patch.alias !== undefined ? (String(patch.alias).trim() || undefined) : current.alias),
@@ -192,24 +227,17 @@ export class ShortTermMemory {
192
227
  next.context = val;
193
228
  }
194
229
  }
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);
230
+ // 更新内存并持久化
231
+ this.sessions.set(id, next);
232
+ await this.persistIndex();
201
233
  return next;
202
234
  }
203
235
  async persistSession(id) {
204
236
  await this.ensureReady();
205
- const current = await this.getSessionMeta(id);
237
+ const current = this.sessions.get(id);
206
238
  if (!current)
207
239
  throw new Error('Session not found');
208
240
  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
241
  let context = '';
214
242
  try {
215
243
  const ctxFile = this.getContextFile(id);
@@ -221,10 +249,8 @@ export class ShortTermMemory {
221
249
  // ignore
222
250
  }
223
251
  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);
252
+ this.sessions.set(id, next);
253
+ await this.persistIndex();
228
254
  await fs.ensureDir(this.getSessionDir(id));
229
255
  await fs.ensureFile(this.getMessagesFile(id));
230
256
  await fs.ensureFile(this.getRunsFile(id));
@@ -241,6 +267,7 @@ export class ShortTermMemory {
241
267
  messages = Array.isArray(data) ? data : [];
242
268
  }
243
269
  catch {
270
+ // 如果消息文件不存在或读取失败,尝试重新创建
244
271
  await fs.ensureDir(this.getSessionDir(id));
245
272
  await fs.writeJson(msgFile, [], { spaces: 2 });
246
273
  messages = [];
@@ -259,8 +286,7 @@ export class ShortTermMemory {
259
286
  }
260
287
  async createRun(sessionId, userContent, runId) {
261
288
  await this.ensureReady();
262
- const meta = await this.getSessionMeta(sessionId);
263
- if (!meta)
289
+ if (!this.sessions.has(sessionId))
264
290
  throw new Error('Session not found');
265
291
  const now = Date.now();
266
292
  const id = runId || uuidv4();
@@ -323,88 +349,91 @@ export class ShortTermMemory {
323
349
  }
324
350
  }
325
351
  async getMessages(sessionId) {
352
+ // 复用 getSession 逻辑,它包含了文件读取和错误处理
326
353
  const session = await this.getSession(sessionId);
327
354
  return session ? session.messages : [];
328
355
  }
329
356
  async addMessage(sessionId, message) {
330
357
  await this.ensureReady();
331
- const session = await this.getSession(sessionId);
332
- if (!session)
358
+ const sessionMeta = this.sessions.get(sessionId);
359
+ if (!sessionMeta)
333
360
  throw new Error(`Session ${sessionId} not found`);
334
361
  const msg = { ...message };
335
362
  if (!msg.timestamp)
336
363
  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));
364
+ // 更新 messages.json
365
+ // 注意:这里仍然存在 messages.json 的并发写入风险,但比 index.json 低,因为通常一个 session 是串行的
366
+ // 如果需要严格安全,也应该为每个 session 维护一个 write queue
367
+ const msgFile = this.getMessagesFile(sessionId);
368
+ let messages = [];
369
+ try {
370
+ const data = await fs.readJson(msgFile);
371
+ messages = Array.isArray(data) ? data : [];
372
+ }
373
+ catch {
374
+ messages = [];
350
375
  }
376
+ messages.push(msg);
377
+ await fs.writeJson(msgFile, messages, { spaces: 2 });
378
+ // 更新 Session Meta
379
+ const nextMeta = {
380
+ ...sessionMeta,
381
+ updatedAt: Date.now(),
382
+ lastActiveAt: Date.now()
383
+ };
384
+ this.sessions.set(sessionId, nextMeta);
385
+ await this.persistIndex();
351
386
  }
352
387
  async clearMessages(sessionId) {
353
388
  await this.ensureReady();
354
- const session = await this.getSession(sessionId);
355
- if (!session)
389
+ const sessionMeta = this.sessions.get(sessionId);
390
+ if (!sessionMeta)
356
391
  throw new Error(`Session ${sessionId} not found`);
357
392
  await fs.writeJson(this.getMessagesFile(sessionId), [], { spaces: 2 });
358
393
  // 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
- }
394
+ const nextMeta = {
395
+ ...sessionMeta,
396
+ updatedAt: Date.now(),
397
+ lastActiveAt: Date.now()
398
+ };
399
+ this.sessions.set(sessionId, nextMeta);
400
+ await this.persistIndex();
370
401
  }
371
402
  async deleteSession(id) {
372
403
  await this.ensureReady();
373
- const metas = await this.loadIndex();
374
- const next = metas.filter(m => m.id !== id);
375
- if (next.length === metas.length)
404
+ if (!this.sessions.has(id))
376
405
  throw new Error('Session not found');
377
- await this.saveIndex(next);
378
- this.metaCache.delete(id);
379
- await fs.remove(this.getSessionDir(id));
406
+ this.sessions.delete(id);
407
+ await this.persistIndex();
408
+ try {
409
+ await fs.remove(this.getSessionDir(id));
410
+ }
411
+ catch (e) {
412
+ console.error(`[ShortTermMemory] Failed to delete session dir ${id}:`, e);
413
+ }
380
414
  }
381
415
  async cleanupOldSessions(maxAgeDays) {
382
416
  await this.ensureReady();
383
- const metas = await this.loadIndex();
384
417
  const now = Date.now();
385
418
  const msPerDay = 24 * 60 * 60 * 1000;
386
- const keep = [];
387
419
  const toDelete = [];
388
- for (const meta of metas) {
420
+ for (const meta of this.sessions.values()) {
389
421
  const updatedAt = typeof meta.updatedAt === 'number' ? meta.updatedAt : 0;
390
422
  const ageDays = (now - updatedAt) / msPerDay;
391
423
  if (ageDays > maxAgeDays)
392
- toDelete.push(meta);
393
- else
394
- keep.push(meta);
424
+ toDelete.push(meta.id);
395
425
  }
396
- for (const meta of toDelete) {
426
+ for (const id of toDelete) {
427
+ this.sessions.delete(id);
397
428
  try {
398
- await fs.remove(this.getSessionDir(meta.id));
399
- this.metaCache.delete(meta.id);
429
+ await fs.remove(this.getSessionDir(id));
400
430
  }
401
431
  catch (e) {
402
- console.error(`[ShortTermMemory] Failed to delete session ${meta.id}:`, e);
432
+ console.error(`[ShortTermMemory] Failed to delete session ${id}:`, e);
403
433
  }
404
434
  }
405
435
  if (toDelete.length > 0) {
406
- keep.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
407
- await this.saveIndex(keep);
436
+ await this.persistIndex();
408
437
  }
409
438
  return toDelete.length;
410
439
  }
@@ -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.71",
6
6
  "author": "mantle.lau",
7
7
  "license": "MIT",
8
8
  "repository": {