evolclaw 2.0.6 → 2.1.0

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.
@@ -30,7 +30,10 @@ export class SessionManager {
30
30
  channel: row.channel,
31
31
  channelId: row.channel_id,
32
32
  projectPath: row.project_path,
33
- claudeSessionId: row.claude_session_id,
33
+ threadId: row.thread_id || '',
34
+ agentType: row.agent_type || 'claude',
35
+ agentSessionId: row.agent_session_id,
36
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
34
37
  name: row.name,
35
38
  isActive: row.is_active === 1,
36
39
  createdAt: row.created_at,
@@ -44,21 +47,21 @@ export class SessionManager {
44
47
  `).run(Date.now(), channel, channelId);
45
48
  }
46
49
  validateSessionFile(row) {
47
- const claudeSessionId = row.claude_session_id;
48
- if (!claudeSessionId)
50
+ const agentSessionId = row.agent_session_id;
51
+ if (!agentSessionId)
49
52
  return undefined;
50
- const sessionFile = this.getSessionFilePath(row.project_path, claudeSessionId);
53
+ const sessionFile = this.getSessionFilePath(row.project_path, agentSessionId);
51
54
  if (fs.existsSync(sessionFile))
52
- return claudeSessionId;
55
+ return agentSessionId;
53
56
  logger.warn(`Session file not found: ${sessionFile}, clearing session ID`);
54
- this.db.prepare(`UPDATE sessions SET claude_session_id = NULL WHERE id = ?`).run(row.id);
57
+ this.db.prepare(`UPDATE sessions SET agent_session_id = NULL WHERE id = ?`).run(row.id);
55
58
  return undefined;
56
59
  }
57
60
  insertSession(session) {
58
61
  this.db.prepare(`
59
- INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at)
60
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
61
- `).run(session.id, session.channel, session.channelId, session.projectPath, session.claudeSessionId ?? null, session.name ?? null, session.isActive ? 1 : 0, session.createdAt, session.updatedAt);
62
+ INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, thread_id, agent_type, agent_session_id, name, is_active, created_at, updated_at, metadata)
63
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
64
+ `).run(session.id, session.channel, session.channelId, session.projectPath, session.threadId || '', session.agentType || 'claude', session.agentSessionId ?? null, session.name ?? null, session.isActive ? 1 : 0, session.createdAt, session.updatedAt, session.metadata ? JSON.stringify(session.metadata) : null);
62
65
  }
63
66
  extractUserMessageText(messageContent) {
64
67
  if (typeof messageContent === 'string') {
@@ -78,55 +81,16 @@ export class SessionManager {
78
81
  const tableInfo = this.db.prepare('PRAGMA table_info(sessions)').all();
79
82
  const hasIsActive = tableInfo.some((col) => col.name === 'is_active');
80
83
  const hasName = tableInfo.some((col) => col.name === 'name');
84
+ const hasThreadId = tableInfo.some((col) => col.name === 'thread_id');
85
+ const hasAgentType = tableInfo.some((col) => col.name === 'agent_type');
86
+ const hasAgentSessionId = tableInfo.some((col) => col.name === 'agent_session_id');
87
+ const hasMetadata = tableInfo.some((col) => col.name === 'metadata');
81
88
  // 检查是否有唯一约束
82
89
  const indexes = this.db.prepare('PRAGMA index_list(sessions)').all();
83
90
  const hasUniqueConstraint = indexes.some((idx) => idx.origin === 'u');
84
- if (!hasIsActive && tableInfo.length > 0) {
85
- logger.info('Migrating database schema (removing unique constraint)...');
86
- this.db.exec(`
87
- CREATE TABLE sessions_new (
88
- id TEXT PRIMARY KEY,
89
- channel TEXT NOT NULL,
90
- channel_id TEXT NOT NULL,
91
- project_path TEXT NOT NULL,
92
- claude_session_id TEXT,
93
- name TEXT,
94
- is_active INTEGER NOT NULL DEFAULT 0,
95
- created_at INTEGER NOT NULL,
96
- updated_at INTEGER NOT NULL
97
- );
98
- INSERT INTO sessions_new SELECT id, channel, channel_id, project_path, claude_session_id, NULL, 1, created_at, updated_at FROM sessions;
99
- DROP TABLE sessions;
100
- ALTER TABLE sessions_new RENAME TO sessions;
101
- `);
102
- logger.info('✓ Database migration completed');
103
- }
104
- else if (!hasName && tableInfo.length > 0) {
105
- logger.info('Adding name column...');
106
- this.db.exec(`ALTER TABLE sessions ADD COLUMN name TEXT`);
107
- if (hasUniqueConstraint) {
108
- logger.info('Removing unique constraint...');
109
- this.db.exec(`
110
- CREATE TABLE sessions_new (
111
- id TEXT PRIMARY KEY,
112
- channel TEXT NOT NULL,
113
- channel_id TEXT NOT NULL,
114
- project_path TEXT NOT NULL,
115
- claude_session_id TEXT,
116
- name TEXT,
117
- is_active INTEGER NOT NULL DEFAULT 0,
118
- created_at INTEGER NOT NULL,
119
- updated_at INTEGER NOT NULL
120
- );
121
- INSERT INTO sessions_new SELECT * FROM sessions;
122
- DROP TABLE sessions;
123
- ALTER TABLE sessions_new RENAME TO sessions;
124
- `);
125
- }
126
- logger.info('✓ Schema updated');
127
- }
128
- else if (hasUniqueConstraint) {
129
- logger.info('Removing stale unique constraint...');
91
+ // 迁移到新表结构(添加 thread_id, agent_type, agent_session_id, metadata)
92
+ if (!hasThreadId && tableInfo.length > 0) {
93
+ logger.info('Migrating database schema (adding thread support)...');
130
94
  this.db.exec(`DROP TABLE IF EXISTS sessions_new`);
131
95
  this.db.exec(`
132
96
  CREATE TABLE sessions_new (
@@ -134,34 +98,51 @@ export class SessionManager {
134
98
  channel TEXT NOT NULL,
135
99
  channel_id TEXT NOT NULL,
136
100
  project_path TEXT NOT NULL,
137
- claude_session_id TEXT,
101
+ thread_id TEXT NOT NULL DEFAULT '',
102
+ agent_type TEXT NOT NULL DEFAULT 'claude',
103
+ agent_session_id TEXT,
138
104
  name TEXT,
139
105
  is_active INTEGER NOT NULL DEFAULT 0,
140
106
  created_at INTEGER NOT NULL,
141
- updated_at INTEGER NOT NULL
107
+ updated_at INTEGER NOT NULL,
108
+ metadata TEXT
142
109
  );
143
- INSERT INTO sessions_new (id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at)
144
- SELECT id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at FROM sessions;
110
+ INSERT INTO sessions_new (id, channel, channel_id, project_path, thread_id, agent_type, agent_session_id, name, is_active, created_at, updated_at, metadata)
111
+ SELECT id, channel, channel_id, project_path, '', 'claude', claude_session_id, name, is_active, created_at, updated_at, NULL FROM sessions;
145
112
  DROP TABLE sessions;
146
113
  ALTER TABLE sessions_new RENAME TO sessions;
147
114
  `);
148
- logger.info('✓ Unique constraint removed');
149
- }
150
- else {
115
+ // 话题会话唯一约束(thread_id 非空时才生效)
151
116
  this.db.exec(`
152
- CREATE TABLE IF NOT EXISTS sessions (
153
- id TEXT PRIMARY KEY,
154
- channel TEXT NOT NULL,
155
- channel_id TEXT NOT NULL,
156
- project_path TEXT NOT NULL,
157
- claude_session_id TEXT,
158
- name TEXT,
159
- is_active INTEGER NOT NULL DEFAULT 0,
160
- created_at INTEGER NOT NULL,
161
- updated_at INTEGER NOT NULL
162
- )
117
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_thread
118
+ ON sessions(channel, channel_id, project_path, thread_id)
119
+ WHERE thread_id != ''
163
120
  `);
121
+ logger.info('✓ Database migration completed (thread support added)');
164
122
  }
123
+ // 创建新表(首次初始化)
124
+ this.db.exec(`
125
+ CREATE TABLE IF NOT EXISTS sessions (
126
+ id TEXT PRIMARY KEY,
127
+ channel TEXT NOT NULL,
128
+ channel_id TEXT NOT NULL,
129
+ project_path TEXT NOT NULL,
130
+ thread_id TEXT NOT NULL DEFAULT '',
131
+ agent_type TEXT NOT NULL DEFAULT 'claude',
132
+ agent_session_id TEXT,
133
+ name TEXT,
134
+ is_active INTEGER NOT NULL DEFAULT 0,
135
+ created_at INTEGER NOT NULL,
136
+ updated_at INTEGER NOT NULL,
137
+ metadata TEXT
138
+ )
139
+ `);
140
+ // 话题会话唯一约束(thread_id 非空时才生效)
141
+ this.db.exec(`
142
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_thread
143
+ ON sessions(channel, channel_id, project_path, thread_id)
144
+ WHERE thread_id != ''
145
+ `);
165
146
  // 创建消息去重表
166
147
  this.db.exec(`
167
148
  CREATE TABLE IF NOT EXISTS processed_messages (
@@ -188,22 +169,28 @@ export class SessionManager {
188
169
  CREATE INDEX IF NOT EXISTS idx_session_health_safe_mode ON session_health(safe_mode);
189
170
  `);
190
171
  }
191
- async getOrCreateSession(channel, channelId, defaultProjectPath, name) {
172
+ async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name) {
173
+ const normalizedThreadId = threadId || '';
174
+ // 话题会话:直接按 thread_id 查找/创建
175
+ if (normalizedThreadId) {
176
+ return this.getOrCreateThreadSession(channel, channelId, normalizedThreadId, defaultProjectPath, metadata, name);
177
+ }
178
+ // 主会话:原有逻辑
192
179
  // 1. 查找该聊天的活跃会话
193
180
  const active = this.db.prepare(`
194
181
  SELECT * FROM sessions
195
- WHERE channel = ? AND channel_id = ? AND is_active = 1
182
+ WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
196
183
  `).get(channel, channelId);
197
184
  if (active) {
198
185
  const validSessionId = this.validateSessionFile(active);
199
- return { ...this.rowToSession(active), claudeSessionId: validSessionId };
186
+ return { ...this.rowToSession(active), agentSessionId: validSessionId };
200
187
  }
201
- // 2. 没有活跃会话,查找该聊天在默认项目的会话
188
+ // 2. 没有活跃会话,查找该聊天在默认项目的会话(匹配 thread_id)
202
189
  const existing = this.db.prepare(`
203
190
  SELECT * FROM sessions
204
- WHERE channel = ? AND channel_id = ? AND project_path = ?
191
+ WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = ?
205
192
  ORDER BY updated_at DESC LIMIT 1
206
- `).get(channel, channelId, defaultProjectPath);
193
+ `).get(channel, channelId, defaultProjectPath, normalizedThreadId);
207
194
  if (existing) {
208
195
  const validSessionId = this.validateSessionFile(existing);
209
196
  // 激活该会话
@@ -211,7 +198,7 @@ export class SessionManager {
211
198
  UPDATE sessions SET is_active = 1, updated_at = ?
212
199
  WHERE id = ?
213
200
  `).run(Date.now(), existing.id);
214
- return { ...this.rowToSession(existing), claudeSessionId: validSessionId, isActive: true };
201
+ return { ...this.rowToSession(existing), agentSessionId: validSessionId, isActive: true };
215
202
  }
216
203
  // 3. 创建新会话(默认为活跃)
217
204
  const session = {
@@ -219,6 +206,9 @@ export class SessionManager {
219
206
  channel,
220
207
  channelId,
221
208
  projectPath: defaultProjectPath,
209
+ threadId: normalizedThreadId,
210
+ agentType: 'claude',
211
+ metadata,
222
212
  name: name || '默认会话',
223
213
  isActive: true,
224
214
  createdAt: Date.now(),
@@ -226,15 +216,15 @@ export class SessionManager {
226
216
  };
227
217
  // 使用 INSERT OR IGNORE 避免并发时的 UNIQUE 约束冲突
228
218
  const result = this.db.prepare(`
229
- INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at)
230
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
231
- `).run(session.id, session.channel, session.channelId, session.projectPath, session.claudeSessionId ?? null, session.name ?? null, 1, session.createdAt, session.updatedAt);
219
+ INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, thread_id, agent_type, agent_session_id, name, is_active, created_at, updated_at, metadata)
220
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
221
+ `).run(session.id, session.channel, session.channelId, session.projectPath, session.threadId, session.agentType, session.agentSessionId ?? null, session.name ?? null, 1, session.createdAt, session.updatedAt, session.metadata ? JSON.stringify(session.metadata) : null);
232
222
  // 如果插入被忽略(已存在),重新查询
233
223
  if (result.changes === 0) {
234
224
  const recheck = this.db.prepare(`
235
225
  SELECT * FROM sessions
236
- WHERE channel = ? AND channel_id = ? AND project_path = ?
237
- `).get(channel, channelId, defaultProjectPath);
226
+ WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = ?
227
+ `).get(channel, channelId, defaultProjectPath, normalizedThreadId);
238
228
  if (recheck) {
239
229
  this.db.prepare(`UPDATE sessions SET is_active = 1, updated_at = ? WHERE id = ?`).run(Date.now(), recheck.id);
240
230
  return { ...this.rowToSession(recheck), isActive: true, updatedAt: Date.now() };
@@ -250,10 +240,60 @@ export class SessionManager {
250
240
  return null;
251
241
  if (typeof v === 'boolean')
252
242
  return v ? 1 : 0;
243
+ if (typeof v === 'object' && v !== null)
244
+ return JSON.stringify(v);
253
245
  return v;
254
246
  });
255
247
  this.db.prepare(`UPDATE sessions SET ${fields}, updated_at = ? WHERE id = ?`).run(...values, Date.now(), sessionId);
256
248
  }
249
+ getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name) {
250
+ const existing = this.db.prepare(`
251
+ SELECT * FROM sessions
252
+ WHERE channel = ? AND channel_id = ? AND thread_id = ?
253
+ LIMIT 1
254
+ `).get(channel, channelId, threadId);
255
+ if (existing) {
256
+ const validSessionId = this.validateSessionFile(existing);
257
+ const existingMeta = this.rowToSession(existing).metadata;
258
+ if (metadata) {
259
+ const merged = existingMeta ? { ...existingMeta, ...metadata } : metadata;
260
+ this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
261
+ .run(JSON.stringify(merged), Date.now(), existing.id);
262
+ return { ...this.rowToSession(existing), agentSessionId: validSessionId, metadata: merged };
263
+ }
264
+ return { ...this.rowToSession(existing), agentSessionId: validSessionId };
265
+ }
266
+ const activeSession = this.db.prepare(`
267
+ SELECT project_path FROM sessions
268
+ WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
269
+ LIMIT 1
270
+ `).get(channel, channelId);
271
+ const projectPath = activeSession?.project_path || defaultProjectPath;
272
+ const session = {
273
+ id: `${channel}-${channelId}-${Date.now()}`,
274
+ channel,
275
+ channelId,
276
+ projectPath,
277
+ threadId,
278
+ agentType: 'claude',
279
+ metadata,
280
+ name: name || '话题会话',
281
+ isActive: false,
282
+ createdAt: Date.now(),
283
+ updatedAt: Date.now()
284
+ };
285
+ this.insertSession(session);
286
+ // Race condition 保护
287
+ const recheck = this.db.prepare(`
288
+ SELECT * FROM sessions
289
+ WHERE channel = ? AND channel_id = ? AND thread_id = ?
290
+ LIMIT 1
291
+ `).get(channel, channelId, threadId);
292
+ if (recheck && recheck.id !== session.id) {
293
+ return this.rowToSession(recheck);
294
+ }
295
+ return session;
296
+ }
257
297
  async switchProject(channel, channelId, newProjectPath) {
258
298
  // 1. 取消当前活跃会话
259
299
  this.deactivateAll(channel, channelId);
@@ -270,7 +310,7 @@ export class SessionManager {
270
310
  UPDATE sessions SET is_active = 1, updated_at = ?
271
311
  WHERE id = ?
272
312
  `).run(Date.now(), target.id);
273
- return { ...this.rowToSession(target), claudeSessionId: validSessionId, isActive: true };
313
+ return { ...this.rowToSession(target), agentSessionId: validSessionId, isActive: true };
274
314
  }
275
315
  // 3. 创建新会话
276
316
  const session = {
@@ -278,6 +318,8 @@ export class SessionManager {
278
318
  channel,
279
319
  channelId,
280
320
  projectPath: newProjectPath,
321
+ threadId: '',
322
+ agentType: 'claude',
281
323
  name: '默认会话',
282
324
  isActive: true,
283
325
  createdAt: Date.now(),
@@ -286,28 +328,28 @@ export class SessionManager {
286
328
  this.insertSession(session);
287
329
  return session;
288
330
  }
289
- async updateClaudeSessionId(channel, channelId, claudeSessionId) {
290
- // 只更新当前活跃会话的 Claude Session ID
331
+ async updateAgentSessionId(channel, channelId, agentSessionId) {
332
+ // 只更新当前活跃会话的 Agent Session ID
291
333
  this.db.prepare(`
292
334
  UPDATE sessions
293
- SET claude_session_id = ?, updated_at = ?
335
+ SET agent_session_id = ?, updated_at = ?
294
336
  WHERE channel = ? AND channel_id = ? AND is_active = 1
295
- `).run(claudeSessionId, Date.now(), channel, channelId);
337
+ `).run(agentSessionId, Date.now(), channel, channelId);
296
338
  }
297
- async updateClaudeSessionIdBySessionId(sessionId, claudeSessionId) {
339
+ async updateAgentSessionIdBySessionId(sessionId, agentSessionId) {
298
340
  // 根据 sessionId 直接更新
299
- logger.info(`[SessionManager] Updating claude_session_id: sessionId=${sessionId}, claudeSessionId=${claudeSessionId}`);
341
+ logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
300
342
  this.db.prepare(`
301
343
  UPDATE sessions
302
- SET claude_session_id = ?, updated_at = ?
344
+ SET agent_session_id = ?, updated_at = ?
303
345
  WHERE id = ?
304
- `).run(claudeSessionId, Date.now(), sessionId);
346
+ `).run(agentSessionId, Date.now(), sessionId);
305
347
  }
306
348
  async clearActiveSession(channel, channelId) {
307
- // 清除当前活跃会话的 Claude Session ID
349
+ // 清除当前活跃会话的 Agent Session ID
308
350
  this.db.prepare(`
309
351
  UPDATE sessions
310
- SET claude_session_id = NULL, updated_at = ?
352
+ SET agent_session_id = NULL, updated_at = ?
311
353
  WHERE channel = ? AND channel_id = ? AND is_active = 1
312
354
  `).run(Date.now(), channel, channelId);
313
355
  }
@@ -378,6 +420,8 @@ export class SessionManager {
378
420
  channel,
379
421
  channelId,
380
422
  projectPath,
423
+ threadId: '',
424
+ agentType: 'claude',
381
425
  name: name || '默认会话',
382
426
  isActive: true,
383
427
  createdAt: Date.now(),
@@ -389,7 +433,7 @@ export class SessionManager {
389
433
  /**
390
434
  * 基于现有会话创建分支会话
391
435
  */
392
- async createForkedSession(sourceSession, forkedClaudeSessionId, name) {
436
+ async createForkedSession(sourceSession, forkedAgentSessionId, name) {
393
437
  // 取消当前活跃会话
394
438
  this.deactivateAll(sourceSession.channel, sourceSession.channelId);
395
439
  const session = {
@@ -397,7 +441,9 @@ export class SessionManager {
397
441
  channel: sourceSession.channel,
398
442
  channelId: sourceSession.channelId,
399
443
  projectPath: sourceSession.projectPath,
400
- claudeSessionId: forkedClaudeSessionId,
444
+ threadId: sourceSession.threadId || '',
445
+ agentType: sourceSession.agentType || 'claude',
446
+ agentSessionId: forkedAgentSessionId,
401
447
  name: name || `${sourceSession.name || '会话'}-分支`,
402
448
  isActive: true,
403
449
  createdAt: Date.now(),
@@ -425,12 +471,12 @@ export class SessionManager {
425
471
  .slice(0, 10);
426
472
  return files.map(f => ({ uuid: f.uuid, mtime: f.mtime }));
427
473
  }
428
- checkSessionFileExists(projectPath, claudeSessionId) {
429
- const sessionFile = this.getSessionFilePath(projectPath, claudeSessionId);
474
+ checkSessionFileExists(projectPath, agentSessionId) {
475
+ const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
430
476
  return fs.existsSync(sessionFile);
431
477
  }
432
- readSessionFirstMessage(projectPath, claudeSessionId) {
433
- const sessionFile = this.getSessionFilePath(projectPath, claudeSessionId);
478
+ readSessionFirstMessage(projectPath, agentSessionId) {
479
+ const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
434
480
  if (!fs.existsSync(sessionFile))
435
481
  return null;
436
482
  try {
@@ -450,8 +496,8 @@ export class SessionManager {
450
496
  }
451
497
  return null;
452
498
  }
453
- readSessionLastUserMessage(projectPath, claudeSessionId) {
454
- const sessionFile = this.getSessionFilePath(projectPath, claudeSessionId);
499
+ readSessionLastUserMessage(projectPath, agentSessionId) {
500
+ const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
455
501
  if (!fs.existsSync(sessionFile))
456
502
  return null;
457
503
  try {
@@ -474,8 +520,8 @@ export class SessionManager {
474
520
  /**
475
521
  * 获取会话文件信息(回合数 + 标题)
476
522
  */
477
- getSessionFileInfo(projectPath, claudeSessionId) {
478
- const sessionFile = this.getSessionFilePath(projectPath, claudeSessionId);
523
+ getSessionFileInfo(projectPath, agentSessionId) {
524
+ const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
479
525
  if (!fs.existsSync(sessionFile))
480
526
  return { turns: 0 };
481
527
  try {
@@ -486,7 +532,12 @@ export class SessionManager {
486
532
  for (const line of lines) {
487
533
  const event = JSON.parse(line);
488
534
  if (event.type === 'user' && event.message?.role === 'user') {
489
- turns++;
535
+ // Only count real user input, skip auto-generated tool_result messages
536
+ const content = event.message.content;
537
+ const isToolResult = Array.isArray(content) && content.every((c) => c.type === 'tool_result');
538
+ if (!isToolResult) {
539
+ turns++;
540
+ }
490
541
  }
491
542
  // 提取会话标题(从 session 元数据中)
492
543
  if (event.title && !title) {
@@ -506,7 +557,7 @@ export class SessionManager {
506
557
  async getSessionByUuidPrefix(channel, channelId, uuidPrefix) {
507
558
  const rows = this.db.prepare(`
508
559
  SELECT * FROM sessions
509
- WHERE channel = ? AND channel_id = ? AND claude_session_id LIKE ?
560
+ WHERE channel = ? AND channel_id = ? AND agent_session_id LIKE ?
510
561
  `).all(channel, channelId, `${uuidPrefix}%`);
511
562
  if (rows.length === 0)
512
563
  return undefined;
@@ -515,23 +566,23 @@ export class SessionManager {
515
566
  }
516
567
  return this.rowToSession(rows[0]);
517
568
  }
518
- async importCliSession(channel, channelId, projectPath, claudeSessionId) {
569
+ async importCliSession(channel, channelId, projectPath, agentSessionId) {
519
570
  // 检查是否已存在相同项目路径的会话
520
571
  const existingByPath = this.db.prepare(`
521
572
  SELECT * FROM sessions
522
573
  WHERE channel = ? AND channel_id = ? AND project_path = ?
523
574
  `).get(channel, channelId, projectPath);
524
575
  if (existingByPath) {
525
- // 更新 claude_session_id 并激活
576
+ // 更新 agent_session_id 并激活
526
577
  this.db.prepare(`
527
578
  UPDATE sessions SET is_active = 0, updated_at = ?
528
579
  WHERE channel = ? AND channel_id = ? AND is_active = 1 AND id != ?
529
580
  `).run(Date.now(), channel, channelId, existingByPath.id);
530
581
  this.db.prepare(`
531
- UPDATE sessions SET claude_session_id = ?, is_active = 1, updated_at = ?
582
+ UPDATE sessions SET agent_session_id = ?, is_active = 1, updated_at = ?
532
583
  WHERE id = ?
533
- `).run(claudeSessionId, Date.now(), existingByPath.id);
534
- return { ...this.rowToSession(existingByPath), claudeSessionId, isActive: true, updatedAt: Date.now() };
584
+ `).run(agentSessionId, Date.now(), existingByPath.id);
585
+ return { ...this.rowToSession(existingByPath), agentSessionId, isActive: true, updatedAt: Date.now() };
535
586
  }
536
587
  // 取消当前活跃会话
537
588
  this.deactivateAll(channel, channelId);
@@ -541,7 +592,9 @@ export class SessionManager {
541
592
  channel,
542
593
  channelId,
543
594
  projectPath,
544
- claudeSessionId,
595
+ threadId: '',
596
+ agentType: 'claude',
597
+ agentSessionId,
545
598
  name: `CLI会话-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`,
546
599
  isActive: true,
547
600
  createdAt: Date.now(),
package/dist/index.js CHANGED
@@ -43,8 +43,8 @@ async function main() {
43
43
  const sessionManager = new SessionManager();
44
44
  logger.info('✓ Database initialized');
45
45
  // 初始化 Agent Runner(带持久化回调)
46
- const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, async (sessionId, claudeSessionId) => {
47
- await sessionManager.updateClaudeSessionIdBySessionId(sessionId, claudeSessionId);
46
+ const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, async (sessionId, agentSessionId) => {
47
+ await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
48
48
  }, anthropic.baseUrl, config);
49
49
  logger.info('✓ Agent runner ready');
50
50
  // 创建消息缓存
@@ -78,7 +78,7 @@ async function main() {
78
78
  // 创建命令处理器
79
79
  const cmdHandler = new CommandHandler(sessionManager, agentRunner, config, messageCache);
80
80
  // 创建消息处理器
81
- const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId) => {
81
+ const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId, threadId) => {
82
82
  const sendFn = async (id, text) => {
83
83
  const adapter = cmdHandler.getAdapter(channel);
84
84
  if (!adapter)
@@ -108,7 +108,7 @@ async function main() {
108
108
  await adapter.sendText(id, text);
109
109
  }
110
110
  };
111
- return cmdHandler.handle(content, channel, channelId, sendFn, userId);
111
+ return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
112
112
  });
113
113
  // 回填 processor 和 messageQueue 的引用
114
114
  cmdHandler.setProcessor(processor);
@@ -223,8 +223,8 @@ async function main() {
223
223
  }
224
224
  // Feishu 消息处理
225
225
  if (feishu) {
226
- feishu.onMessage(async (chatId, content, images, userId, userName, messageId, mentions) => {
227
- content = content.trim();
226
+ feishu.onMessage(async ({ channelId: chatId, content: rawContent, images, userId, userName, messageId, mentions, threadId, rootId }) => {
227
+ let content = rawContent.trim();
228
228
  // 首次交互自动绑定主人
229
229
  if (userId && !config.channels?.feishu?.owner) {
230
230
  const { setOwner } = await import('./config.js');
@@ -233,11 +233,11 @@ async function main() {
233
233
  }
234
234
  // 命令立即处理,不进入队列
235
235
  if (cmdHandler.isCommand(content)) {
236
- const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId);
236
+ const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId, threadId);
237
237
  if (cmdResult !== null) {
238
238
  if (cmdResult) {
239
239
  try {
240
- await feishu.sendMessage(chatId, cmdResult, { forceText: true });
240
+ await feishu.sendMessage(chatId, cmdResult, { forceText: true, replyToMessageId: rootId, replyInThread: true });
241
241
  }
242
242
  catch (error) {
243
243
  logger.error('[Feishu] Failed to send command response:', error);
@@ -246,15 +246,16 @@ async function main() {
246
246
  return;
247
247
  }
248
248
  }
249
- // 获取当前项目路径
250
- const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd());
249
+ // 获取当前项目路径(话题会话自动创建,携带 metadata)
250
+ const metadata = rootId ? { feishu: { rootId } } : undefined;
251
+ const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd(), threadId, metadata);
251
252
  // 群聊消息添加用户名前缀
252
253
  const chatMode = await feishu.getChatMode(chatId);
253
254
  if (chatMode === 'group' && userName) {
254
255
  content = `[${userName}] ${content}`;
255
256
  }
256
- // 普通消息进入队列
257
- await messageQueue.enqueue(`feishu-${chatId}`, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group', mentions }, session.projectPath);
257
+ // 普通消息进入队列(使用 session.id 作为 key,话题间可并行)
258
+ await messageQueue.enqueue(session.id, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group', mentions, threadId }, session.projectPath);
258
259
  });
259
260
  }
260
261
  // AUN 消息处理
@@ -16,9 +16,9 @@ async function fileExists(filePath) {
16
16
  /**
17
17
  * 检查会话文件健康度
18
18
  */
19
- export async function checkSessionFileHealth(projectPath, claudeSessionId) {
19
+ export async function checkSessionFileHealth(projectPath, agentSessionId) {
20
20
  const issues = [];
21
- const sessionFile = path.join(projectPath, '.claude', `${claudeSessionId}.jsonl`);
21
+ const sessionFile = path.join(projectPath, '.claude', `${agentSessionId}.jsonl`);
22
22
  // 检查文件是否存在
23
23
  if (!(await fileExists(sessionFile))) {
24
24
  // 新会话没有文件是正常的
@@ -60,7 +60,8 @@ export async function checkSessionFileHealth(projectPath, claudeSessionId) {
60
60
  */
61
61
  export async function backupClaudeDir(projectPath) {
62
62
  const claudeDir = path.join(projectPath, '.claude');
63
- const backupDir = path.join(claudeDir, `backup-${Date.now()}`);
63
+ const dirName = path.basename(claudeDir);
64
+ const backupDir = path.join(path.dirname(claudeDir), `${dirName}-backup-${Date.now()}`);
64
65
  await fs.cp(claudeDir, backupDir, { recursive: true });
65
66
  logger.info(`[SessionFileHealth] Backup created: ${backupDir}`);
66
67
  return backupDir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.0.6",
3
+ "version": "2.1.0",
4
4
  "description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",