evolclaw 2.8.3 → 3.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.
Files changed (142) hide show
  1. package/README.md +21 -12
  2. package/bin/ec.js +29 -0
  3. package/dist/agents/baseagent-normalize.js +19 -0
  4. package/dist/agents/claude-runner.js +108 -46
  5. package/dist/agents/codex-runner.js +13 -14
  6. package/dist/agents/gemini-runner.js +15 -17
  7. package/dist/agents/kit-renderer.js +281 -0
  8. package/dist/agents/resolve.js +134 -0
  9. package/dist/aun/aid/agentmd.js +186 -0
  10. package/dist/aun/aid/client.js +134 -0
  11. package/dist/aun/aid/identity.js +159 -0
  12. package/dist/aun/aid/index.js +3 -0
  13. package/dist/aun/aid/lifecycle-log.js +33 -0
  14. package/dist/aun/aid/types.js +1 -0
  15. package/dist/aun/aid/validation.js +21 -0
  16. package/dist/aun/msg/group.js +293 -0
  17. package/dist/aun/msg/index.js +4 -0
  18. package/dist/aun/msg/p2p.js +147 -0
  19. package/dist/aun/msg/payload-type.js +27 -0
  20. package/dist/aun/msg/upload.js +98 -0
  21. package/dist/aun/outbox.js +138 -0
  22. package/dist/aun/rpc/caller.js +42 -0
  23. package/dist/aun/rpc/connection.js +34 -0
  24. package/dist/aun/rpc/index.js +2 -0
  25. package/dist/aun/storage/download.js +29 -0
  26. package/dist/aun/storage/index.js +3 -0
  27. package/dist/aun/storage/manage.js +10 -0
  28. package/dist/aun/storage/upload.js +35 -0
  29. package/dist/channels/aun.js +1340 -349
  30. package/dist/channels/dingtalk.js +59 -5
  31. package/dist/channels/feishu.js +381 -32
  32. package/dist/channels/qqbot.js +68 -12
  33. package/dist/channels/wechat.js +63 -4
  34. package/dist/channels/wecom.js +59 -5
  35. package/dist/cli/agent.js +800 -0
  36. package/dist/cli/bench.js +1219 -0
  37. package/dist/cli/index.js +4513 -0
  38. package/dist/{utils → cli}/init-channel.js +211 -621
  39. package/dist/cli/init.js +178 -0
  40. package/dist/cli/link-rules.js +245 -0
  41. package/dist/cli/net-check.js +640 -0
  42. package/dist/cli/watch-msg.js +589 -0
  43. package/dist/config-store.js +645 -0
  44. package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
  45. package/dist/core/channel-loader.js +176 -12
  46. package/dist/core/command-handler.js +883 -848
  47. package/dist/core/evolagent-registry.js +191 -371
  48. package/dist/core/evolagent.js +202 -238
  49. package/dist/core/interaction-router.js +52 -5
  50. package/dist/core/message/im-renderer.js +486 -0
  51. package/dist/core/message/items-formatter.js +68 -0
  52. package/dist/core/message/message-bridge.js +109 -56
  53. package/dist/core/message/message-log.js +93 -0
  54. package/dist/core/message/message-processor.js +430 -212
  55. package/dist/core/message/message-queue.js +13 -6
  56. package/dist/core/permission.js +116 -11
  57. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  58. package/dist/core/session/session-fs-store.js +230 -0
  59. package/dist/core/session/session-manager.js +740 -777
  60. package/dist/core/session/session-mapper.js +87 -0
  61. package/dist/core/trigger/manager.js +122 -0
  62. package/dist/core/trigger/parser.js +128 -0
  63. package/dist/core/trigger/scheduler.js +224 -0
  64. package/dist/data/error-dict.json +118 -0
  65. package/dist/eck/baseagent-caps.js +18 -0
  66. package/dist/eck/detect.js +47 -0
  67. package/dist/eck/init.js +77 -0
  68. package/dist/eck/rules-loader.js +28 -0
  69. package/dist/index.js +560 -283
  70. package/dist/ipc.js +49 -0
  71. package/dist/net-check.js +640 -0
  72. package/dist/paths.js +73 -9
  73. package/dist/types.js +8 -2
  74. package/dist/utils/aid-lifecycle-log.js +33 -0
  75. package/dist/utils/atomic-write.js +89 -0
  76. package/dist/utils/channel-helpers.js +46 -0
  77. package/dist/utils/cross-platform.js +17 -26
  78. package/dist/utils/error-utils.js +10 -2
  79. package/dist/utils/instance-registry.js +434 -0
  80. package/dist/utils/log-writer.js +217 -0
  81. package/dist/utils/logger.js +34 -77
  82. package/dist/utils/media-cache.js +23 -0
  83. package/dist/utils/npm-ops.js +163 -0
  84. package/dist/utils/process-introspect.js +122 -0
  85. package/dist/utils/stats.js +192 -0
  86. package/dist/watch-msg.js +544 -0
  87. package/evolclaw-install-aun.md +127 -47
  88. package/kits/docs/GUIDE.md +20 -0
  89. package/kits/docs/INDEX.md +52 -0
  90. package/kits/docs/aun/CHEATSHEET.md +17 -0
  91. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  92. package/kits/docs/channels/aun.md +25 -0
  93. package/kits/docs/channels/feishu.md +27 -0
  94. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  95. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  96. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  97. package/kits/docs/eck_templates/runtime.template.md +19 -0
  98. package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
  99. package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
  100. package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
  101. package/kits/docs/evolclaw/self-summary.md +29 -0
  102. package/kits/docs/evolclaw/tools.md +25 -0
  103. package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
  104. package/kits/docs/identity/PATH_OPS.md +16 -0
  105. package/kits/docs/identity/ROLE_DETAIL.md +20 -0
  106. package/kits/docs/identity/identity-tools.md +26 -0
  107. package/kits/docs/path-registry.md +43 -0
  108. package/kits/eck_manifest.json +95 -0
  109. package/kits/rules/01-overview.md +120 -0
  110. package/kits/rules/02-navigation.md +75 -0
  111. package/kits/rules/03-identity.md +34 -0
  112. package/kits/rules/04-relation.md +49 -0
  113. package/kits/rules/05-venue.md +45 -0
  114. package/kits/rules/06-channel.md +43 -0
  115. package/kits/templates/system-fragments/baseagent.md +2 -0
  116. package/kits/templates/system-fragments/channel.md +10 -0
  117. package/kits/templates/system-fragments/identity.md +12 -0
  118. package/kits/templates/system-fragments/relation.md +9 -0
  119. package/kits/templates/system-fragments/runtime.md +19 -0
  120. package/kits/templates/system-fragments/venue.md +5 -0
  121. package/package.json +10 -6
  122. package/data/evolclaw.sample.json +0 -60
  123. package/dist/agents/templates.js +0 -122
  124. package/dist/channels/aun-ops.js +0 -275
  125. package/dist/cli.js +0 -2178
  126. package/dist/config.js +0 -591
  127. package/dist/core/agent-registry.js +0 -450
  128. package/dist/core/evolagent-schema.js +0 -72
  129. package/dist/core/message/stream-flusher.js +0 -238
  130. package/dist/core/message/thought-emitter.js +0 -162
  131. package/dist/core/reload-hooks.js +0 -87
  132. package/dist/prompts/templates.js +0 -122
  133. package/dist/templates/prompts.md +0 -104
  134. package/dist/templates/skills.md +0 -66
  135. package/dist/utils/channel-fingerprint.js +0 -59
  136. package/dist/utils/error-dict.js +0 -63
  137. package/dist/utils/format.js +0 -32
  138. package/dist/utils/init.js +0 -645
  139. package/dist/utils/migrate-project.js +0 -122
  140. package/dist/utils/reload-hooks.js +0 -87
  141. package/dist/utils/stats-collector.js +0 -99
  142. package/dist/utils/upgrade.js +0 -100
@@ -1,26 +1,26 @@
1
- import { DatabaseSync } from 'node:sqlite';
2
1
  import { DEFAULT_PERMISSION_MODE } from '../../types.js';
3
- import { ensureDir } from '../../config.js';
4
- import { resolvePaths } from '../../paths.js';
2
+ import { ensureDir } from '../../utils/atomic-write.js';
5
3
  import { logger } from '../../utils/logger.js';
6
4
  import { encodePath } from '../../utils/cross-platform.js';
5
+ import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, appendJsonl, readJsonFile, readLastJsonlLine, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
6
+ import { sessionToFile, fileToSession } from './session-mapper.js';
7
7
  import path from 'path';
8
+ import fs from 'fs';
8
9
  import os from 'os';
9
10
  export class SessionManager {
10
- db;
11
+ sessionsDir;
11
12
  eventBus;
12
13
  ownerResolver;
13
14
  adminResolver;
14
15
  sessionModeResolver;
15
16
  fileAdapters = new Map();
16
17
  sessionEncryptState = new Map();
17
- constructor(dbPath = resolvePaths().db, eventBus, ownerResolver, adminResolver) {
18
- ensureDir(path.dirname(dbPath));
19
- this.db = new DatabaseSync(dbPath);
18
+ constructor(sessionsDir, eventBus, ownerResolver, adminResolver) {
19
+ ensureDir(sessionsDir);
20
+ this.sessionsDir = sessionsDir;
20
21
  this.eventBus = eventBus;
21
22
  this.ownerResolver = ownerResolver;
22
23
  this.adminResolver = adminResolver;
23
- this.initDatabase();
24
24
  }
25
25
  setOwnerResolver(resolver) {
26
26
  this.ownerResolver = resolver;
@@ -31,8 +31,10 @@ export class SessionManager {
31
31
  setSessionModeResolver(resolver) {
32
32
  this.sessionModeResolver = resolver;
33
33
  }
34
- /** 解析默认 sessionMode:通道配置锁定 > chatType 默认 > 'interactive' */
35
- resolveDefaultSessionMode(channel, chatType) {
34
+ resolveDefaultSessionMode(channel, chatType, peerType) {
35
+ // human 对端(ai/bot)强制 proactive,无视 agent 的默认 chatmode 配置
36
+ if (peerType && peerType !== 'human' && peerType !== 'unknown')
37
+ return 'proactive';
36
38
  const ct = chatType || 'private';
37
39
  const resolved = this.sessionModeResolver?.(channel, ct);
38
40
  return resolved || 'interactive';
@@ -44,9 +46,6 @@ export class SessionManager {
44
46
  getFileAdapter(agentId) {
45
47
  return this.fileAdapters.get(agentId);
46
48
  }
47
- getDatabase() {
48
- return this.db;
49
- }
50
49
  getProjectDirName(projectPath) {
51
50
  return encodePath(projectPath);
52
51
  }
@@ -55,27 +54,6 @@ export class SessionManager {
55
54
  const encodedPath = this.getProjectDirName(projectPath);
56
55
  return path.join(homeDir, '.claude', 'projects', encodedPath, `${sessionId}.jsonl`);
57
56
  }
58
- rowToSession(row) {
59
- const metadata = row.metadata ? JSON.parse(row.metadata) : undefined;
60
- return {
61
- id: row.id,
62
- channel: row.channel,
63
- channelId: row.channel_id,
64
- projectPath: row.project_path,
65
- threadId: row.thread_id || '',
66
- agentId: row.agent_id || 'claude',
67
- chatType: row.chat_type || 'private',
68
- sessionMode: row.session_mode || 'interactive',
69
- agentSessionId: row.agent_session_id,
70
- metadata,
71
- name: row.name,
72
- processingState: row.processing_state || undefined,
73
- createdAt: row.created_at,
74
- updatedAt: row.updated_at,
75
- deletedAt: row.deleted_at ?? undefined,
76
- };
77
- }
78
- /** 根据 userId 计算身份 */
79
57
  resolveIdentity(channel, userId) {
80
58
  if (!userId)
81
59
  return { role: 'anonymous', mode: 'interactive' };
@@ -85,58 +63,9 @@ export class SessionManager {
85
63
  return { role: 'admin', mode: 'interactive' };
86
64
  return { role: 'guest', mode: 'interactive' };
87
65
  }
88
- /** 更新 session 的 identity(owner 绑定后调用) */
89
66
  async updateIdentity(sessionId, identity) {
90
- // identity 不持久化到 DB,仅更新内存中的返回值
91
- // 调用方应直接修改持有的 session 对象
92
67
  logger.debug(`[SessionManager] updateIdentity: sessionId=${sessionId}, role=${identity.role}`);
93
68
  }
94
- /** 取消所有活跃会话(通过 metadata.isActive) */
95
- deactivateAllMetadata(channel, channelId) {
96
- const rows = this.db.prepare(`
97
- SELECT id, metadata FROM sessions
98
- WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true
99
- `).all(channel, channelId);
100
- for (const row of rows) {
101
- const metadata = row.metadata ? JSON.parse(row.metadata) : {};
102
- metadata.isActive = false;
103
- this.db.prepare(`
104
- UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?
105
- `).run(JSON.stringify(metadata), Date.now(), row.id);
106
- }
107
- }
108
- /** 获取当前活跃 session 的 chatType(用于新建 session 时继承) */
109
- getActiveChatType(channel, channelId) {
110
- const row = this.db.prepare(`
111
- SELECT chat_type FROM sessions
112
- WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true AND thread_id = '' AND deleted_at IS NULL
113
- ORDER BY updated_at DESC LIMIT 1
114
- `).get(channel, channelId);
115
- return row?.chat_type || 'private';
116
- }
117
- validateSessionFile(row) {
118
- const agentSessionId = row.agent_session_id;
119
- if (!agentSessionId)
120
- return undefined;
121
- const agentId = row.agent_id || 'claude';
122
- const adapter = this.getFileAdapter(agentId);
123
- if (!adapter) {
124
- // 无适配器:无法验证文件,信任 DB 记录
125
- return agentSessionId;
126
- }
127
- if (adapter.checkExists(row.project_path, agentSessionId)) {
128
- return agentSessionId;
129
- }
130
- logger.warn(`Session file not found for ${agentId}: ${agentSessionId}, clearing session ID`);
131
- this.db.prepare(`UPDATE sessions SET agent_session_id = NULL WHERE id = ?`).run(row.id);
132
- return undefined;
133
- }
134
- insertSession(session) {
135
- this.db.prepare(`
136
- INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, thread_id, agent_id, chat_type, session_mode, agent_session_id, name, created_at, updated_at, metadata)
137
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
138
- `).run(session.id, session.channel, session.channelId, session.projectPath, session.threadId || '', session.agentId || 'claude', session.chatType || 'private', session.sessionMode || 'interactive', session.agentSessionId ?? null, session.name ?? null, session.createdAt, session.updatedAt, session.metadata ? JSON.stringify(session.metadata) : null);
139
- }
140
69
  extractUserMessageText(messageContent) {
141
70
  if (typeof messageContent === 'string') {
142
71
  const text = messageContent.trim().replace(/\s+/g, ' ');
@@ -151,306 +80,290 @@ export class SessionManager {
151
80
  }
152
81
  return null;
153
82
  }
154
- initDatabase() {
155
- const tableInfo = this.db.prepare('PRAGMA table_info(sessions)').all();
156
- const hasIsActive = tableInfo.some((col) => col.name === 'is_active');
157
- const hasName = tableInfo.some((col) => col.name === 'name');
158
- const hasThreadId = tableInfo.some((col) => col.name === 'thread_id');
159
- const hasAgentType = tableInfo.some((col) => col.name === 'agent_type');
160
- const hasAgentId = tableInfo.some((col) => col.name === 'agent_id');
161
- const hasAgentSessionId = tableInfo.some((col) => col.name === 'agent_session_id');
162
- const hasMetadata = tableInfo.some((col) => col.name === 'metadata');
163
- const hasIsGroup = tableInfo.some((col) => col.name === 'is_group');
164
- const hasChatType = tableInfo.some((col) => col.name === 'chat_type');
165
- const hasSessionMode = tableInfo.some((col) => col.name === 'session_mode');
166
- const hasDeletedAt = tableInfo.some((col) => col.name === 'deleted_at');
167
- // 检测是否需要 schema 重构迁移(旧字段存在,新字段不存在)
168
- const needsSchemaRefactor = tableInfo.length > 0 && (hasIsGroup || hasIsActive || hasAgentType) && (!hasChatType || !hasAgentId || !hasSessionMode);
169
- // Schema 重构迁移:is_group → chat_type, agent_type → agent_id, 移除 is_active
170
- if (needsSchemaRefactor) {
171
- logger.info('Migrating database schema (session model refactor)...');
172
- this.db.exec(`DROP TABLE IF EXISTS sessions_new`);
173
- this.db.exec(`
174
- CREATE TABLE sessions_new (
175
- id TEXT PRIMARY KEY,
176
- channel TEXT NOT NULL,
177
- channel_id TEXT NOT NULL,
178
- agent_id TEXT NOT NULL DEFAULT 'claude',
179
- thread_id TEXT NOT NULL DEFAULT '',
180
- chat_type TEXT NOT NULL DEFAULT 'private',
181
- session_mode TEXT NOT NULL DEFAULT 'interactive',
182
- project_path TEXT NOT NULL,
183
- agent_session_id TEXT,
184
- name TEXT,
185
- created_at INTEGER NOT NULL,
186
- updated_at INTEGER NOT NULL,
187
- metadata TEXT,
188
- deleted_at INTEGER
189
- )
190
- `);
191
- // 迁移数据:is_group → chat_type, agent_type → agent_id
192
- this.db.exec(`
193
- INSERT INTO sessions_new (id, channel, channel_id, agent_id, thread_id, chat_type, session_mode, project_path, agent_session_id, name, created_at, updated_at, metadata, deleted_at)
194
- SELECT
195
- id,
196
- channel,
197
- channel_id,
198
- COALESCE(agent_type, 'claude'),
199
- COALESCE(thread_id, ''),
200
- CASE WHEN is_group = 1 THEN 'group' ELSE 'private' END,
201
- 'interactive',
202
- project_path,
203
- agent_session_id,
204
- name,
205
- created_at,
206
- updated_at,
207
- metadata,
208
- deleted_at
209
- FROM sessions
210
- `);
211
- this.db.exec(`DROP TABLE sessions`);
212
- this.db.exec(`ALTER TABLE sessions_new RENAME TO sessions`);
213
- // 创建新索引
214
- this.db.exec(`
215
- CREATE INDEX IF NOT EXISTS idx_session_space
216
- ON sessions(channel, channel_id, agent_id, thread_id)
217
- WHERE deleted_at IS NULL
218
- `);
219
- this.db.exec(`
220
- CREATE INDEX IF NOT EXISTS idx_session_active
221
- ON sessions(channel, channel_id)
222
- WHERE deleted_at IS NULL
223
- `);
224
- logger.info('✓ Database migration completed (session model refactored)');
83
+ // ─── File I/O helpers ───
84
+ /**
85
+ * 解析 chat 目录路径。
86
+ * 1. 先扫描所有 chat 目录,按 channelId 查找匹配项(同时 channelType==channel 或缺失时直接 channelId 匹配)
87
+ * 2. 找不到则按 fallback:channelType=channel(实例名),selfId=null
88
+ *
89
+ * 这样保持兼容:不知道 channelType caller 仍可以用 (channel, channelId) 调用。
90
+ */
91
+ resolveChatDir(channel, channelId) {
92
+ // 优先尝试从已有目录里找
93
+ const dirs = scanChatDirs(this.sessionsDir);
94
+ for (const d of dirs) {
95
+ if (d.channelId !== channelId)
96
+ continue;
97
+ // 验证 active.json meta 文件里 channel(实例名)匹配
98
+ const active = readJsonFile(path.join(d.dirPath, 'active.json'));
99
+ if (active && active.channel === channel)
100
+ return d.dirPath;
101
+ // 没 active.json 时,看 channelType 是否能匹配 channel
102
+ if (!active && d.channelType === channel)
103
+ return d.dirPath;
225
104
  }
226
- // ── schema 迁移(仅当旧字段存在、新字段还未迁移时运行)──
227
- // 这些迁移按顺序将最旧的 schema 逐步升级到包含 is_group 的中间格式,
228
- // 然后由上面的 needsSchemaRefactor 迁移一步到位转为新 schema。
229
- if (!needsSchemaRefactor && tableInfo.length > 0 && hasAgentType) {
230
- // 检查是否有唯一约束
231
- const indexes = this.db.prepare('PRAGMA index_list(sessions)').all();
232
- const hasUniqueConstraint = indexes.some((idx) => idx.origin === 'u');
233
- // 迁移到新表结构(添加 thread_id, agent_type, agent_session_id, metadata)
234
- if (!hasThreadId) {
235
- logger.info('Migrating database schema (adding thread support)...');
236
- this.db.exec(`DROP TABLE IF EXISTS sessions_new`);
237
- this.db.exec(`
238
- CREATE TABLE sessions_new (
239
- id TEXT PRIMARY KEY,
240
- channel TEXT NOT NULL,
241
- channel_id TEXT NOT NULL,
242
- project_path TEXT NOT NULL,
243
- thread_id TEXT NOT NULL DEFAULT '',
244
- agent_type TEXT NOT NULL DEFAULT 'claude',
245
- agent_session_id TEXT,
246
- name TEXT,
247
- is_active INTEGER NOT NULL DEFAULT 0,
248
- created_at INTEGER NOT NULL,
249
- updated_at INTEGER NOT NULL,
250
- metadata TEXT
251
- );
252
- 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)
253
- SELECT id, channel, channel_id, project_path, '', 'claude', claude_session_id, name, is_active, created_at, updated_at, NULL FROM sessions;
254
- DROP TABLE sessions;
255
- ALTER TABLE sessions_new RENAME TO sessions;
256
- `);
257
- // 话题会话唯一约束(thread_id 非空时才生效)
258
- this.db.exec(`
259
- CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_thread
260
- ON sessions(channel, channel_id, project_path, thread_id)
261
- WHERE thread_id != ''
262
- `);
263
- logger.info('✓ Database migration completed (thread support added)');
264
- }
265
- // Migration: add is_group column
266
- if (!hasIsGroup) {
267
- logger.info('Migrating database schema (adding is_group)...');
268
- const addIsGroupCol = 'ALTER TABLE sessions ADD COLUMN is_group INTEGER NOT NULL DEFAULT 0';
269
- this.db.exec(addIsGroupCol);
270
- logger.info('✓ Database migration completed (is_group added)');
271
- }
272
- // Reset incorrect is_group values (oc_ prefix doesn't reliably indicate group chat)
273
- if (hasIsGroup) {
274
- this.db.exec("UPDATE sessions SET is_group = 0 WHERE channel = 'feishu'");
275
- }
276
- // Migration: add deleted_at column
277
- if (!hasDeletedAt) {
278
- logger.info('Migrating database schema (adding deleted_at)...');
279
- this.db.exec(`ALTER TABLE sessions ADD COLUMN deleted_at INTEGER`);
280
- logger.info('✓ Database migration completed (deleted_at added)');
281
- }
105
+ // Fallback:按 channel channelType 创建(旧路径布局兼容)
106
+ return chatDirPath(this.sessionsDir, channel, channelId);
107
+ }
108
+ /**
109
+ * 给定明确的 channelType + selfId 时直接计算路径(不扫描)。
110
+ * 用于 caller 已经知道完整路由信息的场景(如 message-bridge 透传)。
111
+ */
112
+ resolveChatDirExact(channel, channelId, channelType, selfId) {
113
+ if (channelType) {
114
+ return chatDirPath(this.sessionsDir, channelType, channelId, selfId);
282
115
  }
283
- // Migration: add processing_state column (独立于 schema 重构)
284
- if (tableInfo.length > 0) {
285
- const hasProcessingState = tableInfo.some((col) => col.name === 'processing_state');
286
- if (!hasProcessingState) {
287
- logger.info('Migrating database schema (adding processing_state)...');
288
- this.db.exec(`ALTER TABLE sessions ADD COLUMN processing_state TEXT`);
289
- logger.info('✓ Database migration completed (processing_state added)');
290
- }
116
+ return this.resolveChatDir(channel, channelId);
117
+ }
118
+ resolveChatDirFromSession(session) {
119
+ const channelType = session.channelType || session.channel;
120
+ return chatDirPath(this.sessionsDir, channelType, session.channelId, session.selfId);
121
+ }
122
+ /** Public accessor: get the chat directory path for a session (for message log etc.) */
123
+ getChatDir(session) {
124
+ return this.resolveChatDirFromSession(session);
125
+ }
126
+ /** Like resolveChatDir but also ensures the dir + _threads + _trash exist. */
127
+ ensureResolvedChatDir(channel, channelId) {
128
+ const dir = this.resolveChatDir(channel, channelId);
129
+ fs.mkdirSync(dir, { recursive: true });
130
+ fs.mkdirSync(path.join(dir, '_threads'), { recursive: true });
131
+ fs.mkdirSync(path.join(dir, '_trash'), { recursive: true });
132
+ return dir;
133
+ }
134
+ /** 推断给定 chat 的 channelType(优先取 active.json)。无活跃时回落到 channel 实例名。 */
135
+ inferChannelType(channel, channelId) {
136
+ const active = this.readActive(channel, channelId);
137
+ return active?.channelType || channel;
138
+ }
139
+ /** 从 active 推断 selfId(已有 session 的复用) */
140
+ inferSelfId(channel, channelId) {
141
+ const active = this.readActive(channel, channelId);
142
+ return active?.selfId;
143
+ }
144
+ readActive(channel, channelId) {
145
+ const dir = this.resolveChatDir(channel, channelId);
146
+ const file = readJsonFile(path.join(dir, 'active.json'));
147
+ if (!file)
148
+ return undefined;
149
+ return fileToSession(file);
150
+ }
151
+ writeActive(channel, channelId, session) {
152
+ const dir = this.ensureChatDirForSession(session);
153
+ const file = sessionToFile(session);
154
+ atomicWriteJson(path.join(dir, 'active.json'), file);
155
+ }
156
+ clearActive(channel, channelId) {
157
+ const dir = this.resolveChatDir(channel, channelId);
158
+ const activePath = path.join(dir, 'active.json');
159
+ try {
160
+ fs.unlinkSync(activePath);
291
161
  }
292
- // Migration: normalize legacy metadata rootId → replyContext
293
- if (hasMetadata && tableInfo.length > 0) {
294
- const rows = this.db.prepare(`SELECT id, metadata FROM sessions WHERE metadata IS NOT NULL AND metadata != ''`).all();
295
- let migrated = 0;
296
- for (const row of rows) {
297
- try {
298
- const meta = JSON.parse(row.metadata);
299
- const rootId = meta.feishu?.rootId ?? meta.threadRootId ?? meta.replyOpts?.rootId;
300
- if (!rootId && !meta.feishu && !meta.threadRootId && !meta.replyOpts)
301
- continue;
302
- // Generate replyContext from rootId if missing
303
- if (rootId && !meta.replyContext) {
304
- meta.replyContext = { replyToMessageId: rootId, replyInThread: true };
305
- }
306
- // Clean up all legacy fields
307
- delete meta.feishu;
308
- delete meta.threadRootId;
309
- delete meta.replyOpts;
310
- this.db.prepare('UPDATE sessions SET metadata = ? WHERE id = ?')
311
- .run(JSON.stringify(meta), row.id);
312
- migrated++;
313
- }
314
- catch { /* skip malformed JSON */ }
315
- }
316
- if (migrated > 0) {
317
- logger.info(`✓ Migrated ${migrated} session(s): rootId normalized to replyContext`);
318
- }
162
+ catch (e) {
163
+ if (e.code !== 'ENOENT')
164
+ throw e;
319
165
  }
320
- // Migration: readonly 模式已禁用,历史会话统一转为 auto
321
- if (hasMetadata && tableInfo.length > 0) {
322
- const rows = this.db.prepare(`SELECT id, metadata FROM sessions WHERE metadata IS NOT NULL AND metadata != ''`).all();
323
- let migratedPerm = 0;
324
- for (const row of rows) {
325
- try {
326
- const meta = JSON.parse(row.metadata);
327
- if (meta.permissionMode === 'readonly' || meta.permissionMode === 'noask') {
328
- meta.permissionMode = 'auto';
329
- this.db.prepare('UPDATE sessions SET metadata = ? WHERE id = ?')
330
- .run(JSON.stringify(meta), row.id);
331
- migratedPerm++;
332
- }
333
- }
334
- catch { /* skip malformed JSON */ }
335
- }
336
- if (migratedPerm > 0) {
337
- logger.info(`✓ Migrated ${migratedPerm} session(s): permissionMode auto`);
166
+ }
167
+ ensureChatDirForSession(session) {
168
+ const channelType = session.channelType || session.channel;
169
+ return ensureChatDir(this.sessionsDir, channelType, session.channelId, session.selfId);
170
+ }
171
+ metaFilePath(chatDir, sessionId) {
172
+ return path.join(chatDir, `${sessionId}.jsonl`);
173
+ }
174
+ appendMeta(channel, channelId, session) {
175
+ const dir = this.ensureChatDirForSession(session);
176
+ const isThread = !!session.threadId;
177
+ const targetDir = isThread ? path.join(dir, '_threads') : dir;
178
+ const file = sessionToFile(session);
179
+ const metaPath = this.metaFilePath(targetDir, session.id);
180
+ appendJsonl(metaPath, file);
181
+ }
182
+ /**
183
+ * 比较两个 SessionFile 是否在内容上相等(忽略 updatedAt / updatedAtStr)。
184
+ * 用于跳过"没真变化"的写入,避免 jsonl 写放大。
185
+ */
186
+ sessionFilesEqual(a, b) {
187
+ const stripVolatile = ({ updatedAt, updatedAtStr, ...rest }) => rest;
188
+ return JSON.stringify(stripVolatile(a)) === JSON.stringify(stripVolatile(b));
189
+ }
190
+ /**
191
+ * Append meta + write active.json,但只在 session 内容(除 updatedAt 外)真正变化时才写。
192
+ * prev 是修改前的快照(用于 diff),next 是修改后的 session。
193
+ * 返回是否发生了写入。
194
+ */
195
+ writeSessionIfChanged(channel, channelId, prev, next) {
196
+ if (prev) {
197
+ const prevFile = sessionToFile(prev);
198
+ const nextFile = sessionToFile(next);
199
+ if (this.sessionFilesEqual(prevFile, nextFile))
200
+ return false;
201
+ }
202
+ next.updatedAt = Date.now();
203
+ this.appendMeta(channel, channelId, next);
204
+ const active = this.readActive(channel, channelId);
205
+ if (active && active.id === next.id) {
206
+ // 保留 active.json 中已有的 activeTask(markProcessing 写入的处理状态)
207
+ if (active.processingState && !next.processingState) {
208
+ next.processingState = active.processingState;
338
209
  }
210
+ this.writeActive(channel, channelId, next);
339
211
  }
340
- // 创建新表(首次初始化)
341
- this.db.exec(`
342
- CREATE TABLE IF NOT EXISTS sessions (
343
- id TEXT PRIMARY KEY,
344
- channel TEXT NOT NULL,
345
- channel_id TEXT NOT NULL,
346
- agent_id TEXT NOT NULL DEFAULT 'claude',
347
- thread_id TEXT NOT NULL DEFAULT '',
348
- chat_type TEXT NOT NULL DEFAULT 'private',
349
- session_mode TEXT NOT NULL DEFAULT 'interactive',
350
- project_path TEXT NOT NULL,
351
- agent_session_id TEXT,
352
- name TEXT,
353
- processing_state TEXT,
354
- created_at INTEGER NOT NULL,
355
- updated_at INTEGER NOT NULL,
356
- metadata TEXT,
357
- deleted_at INTEGER
358
- )
359
- `);
360
- // 会话空间索引(查询优化,无唯一约束)
361
- this.db.exec(`
362
- CREATE INDEX IF NOT EXISTS idx_session_space
363
- ON sessions(channel, channel_id, agent_id, thread_id)
364
- WHERE deleted_at IS NULL
365
- `);
366
- this.db.exec(`
367
- CREATE INDEX IF NOT EXISTS idx_session_active
368
- ON sessions(channel, channel_id)
369
- WHERE deleted_at IS NULL
370
- `);
371
- // 创建消息去重表
372
- this.db.exec(`
373
- CREATE TABLE IF NOT EXISTS processed_messages (
374
- message_id TEXT PRIMARY KEY,
375
- channel TEXT NOT NULL,
376
- channel_id TEXT NOT NULL,
377
- processed_at INTEGER NOT NULL
378
- );
379
- CREATE INDEX IF NOT EXISTS idx_processed_at ON processed_messages(processed_at);
380
- `);
381
- // 创建会话健康状态表
382
- this.db.exec(`
383
- CREATE TABLE IF NOT EXISTS session_health (
384
- session_id TEXT PRIMARY KEY,
385
- consecutive_errors INTEGER NOT NULL DEFAULT 0,
386
- last_error TEXT,
387
- last_error_type TEXT,
388
- safe_mode INTEGER NOT NULL DEFAULT 0,
389
- last_success_time INTEGER NOT NULL,
390
- created_at INTEGER NOT NULL,
391
- updated_at INTEGER NOT NULL,
392
- FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
393
- );
394
- CREATE INDEX IF NOT EXISTS idx_session_health_safe_mode ON session_health(safe_mode);
395
- `);
212
+ return true;
213
+ }
214
+ readMetaLatest(metaFilePath) {
215
+ const file = readLastJsonlLine(metaFilePath);
216
+ if (!file)
217
+ return undefined;
218
+ return fileToSession(file);
396
219
  }
397
220
  /**
398
- * 启动时迁移:将 sessions.channel channelType 回填为实例名(channelName)。
399
- * 读取每行 metadata.channelName,若与 channel 列不同则更新。
221
+ * by-sessionId 改方法加载"当前 session 状态"。
222
+ *
223
+ * 设计契约(docs/refactor/01-db-to-fs.md):
224
+ * active.json 是热路径权威源。.jsonl 是历史档案。
225
+ *
226
+ * 读取策略:
227
+ * 1. 先按 sessionId 定位 .jsonl 文件(确认 session 存在 + 拿到 channel/channelId)
228
+ * 2. 优先读 active.json(如果 active.id === sessionId)—— 当前状态
229
+ * 3. 否则 fallback 到 .jsonl 末行 —— 非活跃 session 的更新(如多 session 并存时改非 active 那个)
230
+ *
231
+ * 返回 { current, prev }:
232
+ * - current 用于 caller 修改后写回
233
+ * - prev 是 current 的初始快照(用于 writeSessionIfChanged 的 diff 检查)
400
234
  */
401
- migrateChannelToInstanceName() {
402
- const rows = this.db.prepare(`SELECT id, channel, metadata FROM sessions WHERE metadata IS NOT NULL AND metadata != ''`).all();
403
- let migrated = 0;
404
- for (const row of rows) {
405
- try {
406
- const meta = JSON.parse(row.metadata);
407
- if (meta.channelName && meta.channelName !== row.channel) {
408
- this.db.prepare(`UPDATE sessions SET channel = ?, updated_at = ? WHERE id = ?`)
409
- .run(meta.channelName, Date.now(), row.id);
410
- migrated++;
411
- logger.info(`[Migration] Restored channel '${row.channel}' -> '${meta.channelName}' (session ${row.id})`);
412
- }
235
+ loadSessionForUpdate(sessionId) {
236
+ const found = this.findSessionFileById(sessionId);
237
+ if (!found)
238
+ return undefined;
239
+ // 先读 .jsonl 末行拿 channel/channelId(active.json 文件路径需要这两个)
240
+ const fromJsonl = this.readMetaLatest(found.metaPath);
241
+ if (!fromJsonl)
242
+ return undefined;
243
+ // 优先用 active.json 的当前状态(如果它就是这个 sessionId)
244
+ const active = this.readActive(fromJsonl.channel, fromJsonl.channelId);
245
+ const base = (active && active.id === sessionId) ? active : fromJsonl;
246
+ // 深拷贝避免 caller 改 current 时污染 prev
247
+ const current = JSON.parse(JSON.stringify(base));
248
+ const prev = JSON.parse(JSON.stringify(base));
249
+ return { current, prev };
250
+ }
251
+ validateSessionFile(session) {
252
+ const agentSessionId = session.agentSessionId;
253
+ if (!agentSessionId)
254
+ return undefined;
255
+ const agentId = session.agentId || 'claude';
256
+ const adapter = this.getFileAdapter(agentId);
257
+ if (!adapter)
258
+ return agentSessionId;
259
+ if (adapter.checkExists(session.projectPath, agentSessionId))
260
+ return agentSessionId;
261
+ logger.warn(`Session file not found for ${agentId}: ${agentSessionId}, clearing session ID`);
262
+ const prev = JSON.parse(JSON.stringify(session));
263
+ session.agentSessionId = undefined;
264
+ this.writeSessionIfChanged(session.channel, session.channelId, prev, session);
265
+ return undefined;
266
+ }
267
+ getActiveChatType(channel, channelId) {
268
+ const active = this.readActive(channel, channelId);
269
+ if (active && !active.threadId)
270
+ return active.chatType || 'private';
271
+ return 'private';
272
+ }
273
+ findAllSessionsInChat(chatDir, includeThreads = true) {
274
+ const metaFiles = scanMetaFiles(chatDir);
275
+ const results = [];
276
+ for (const metaFile of metaFiles) {
277
+ const session = this.readMetaLatest(path.join(chatDir, metaFile));
278
+ if (session)
279
+ results.push(session);
280
+ }
281
+ if (includeThreads) {
282
+ const threadsDir = path.join(chatDir, '_threads');
283
+ const threadMetas = scanMetaFiles(threadsDir);
284
+ for (const metaFile of threadMetas) {
285
+ const session = this.readMetaLatest(path.join(threadsDir, metaFile));
286
+ if (session)
287
+ results.push(session);
413
288
  }
414
- catch { /* skip invalid metadata */ }
415
289
  }
416
- if (migrated > 0) {
417
- logger.info(`Channel instance name migration completed (${migrated} sessions updated)`);
290
+ return results;
291
+ }
292
+ findSessionFileById(sessionId) {
293
+ const chatDirs = scanChatDirs(this.sessionsDir);
294
+ for (const { dirPath } of chatDirs) {
295
+ const mainPath = path.join(dirPath, `${sessionId}.jsonl`);
296
+ try {
297
+ fs.statSync(mainPath);
298
+ return { chatDir: dirPath, metaPath: mainPath, isThread: false };
299
+ }
300
+ catch { }
301
+ const threadPath = path.join(dirPath, '_threads', `${sessionId}.jsonl`);
302
+ try {
303
+ fs.statSync(threadPath);
304
+ return { chatDir: dirPath, metaPath: threadPath, isThread: true };
305
+ }
306
+ catch { }
418
307
  }
308
+ return undefined;
419
309
  }
420
- /**
421
- * 获取指定渠道所有已知的 thread_id(用于重启后预填充 seenThreads)
422
- */
310
+ // ─── Public API ───
423
311
  getKnownThreadIds(channel) {
424
- const rows = this.db.prepare(`
425
- SELECT DISTINCT thread_id FROM sessions
426
- WHERE channel = ? AND thread_id != '' AND deleted_at IS NULL
427
- `).all(channel);
428
- return rows.map(r => r.thread_id);
312
+ const chatDirs = scanChatDirs(this.sessionsDir);
313
+ const threadIds = [];
314
+ for (const { dirPath } of chatDirs) {
315
+ const active = readJsonFile(path.join(dirPath, 'active.json'));
316
+ const matchInstance = active?.channel === channel;
317
+ // 也兼容没 active.json 时按目录顶层 channelType 匹配(fallback 路径布局)
318
+ if (!matchInstance)
319
+ continue;
320
+ const index = readThreadIndex(dirPath);
321
+ for (const tid of Object.keys(index))
322
+ threadIds.push(tid);
323
+ }
324
+ return threadIds;
429
325
  }
430
- /**
431
- * 标记会话为处理中(实时写 DB,crash 也能恢复)
432
- * processing_state 格式: "timestamp:taskId"
433
- */
434
326
  markProcessing(sessionId, taskId) {
435
327
  const now = Date.now();
436
328
  const state = taskId ? `${now}:${taskId}` : String(now);
437
- this.db.prepare(`UPDATE sessions SET processing_state = ?, updated_at = ? WHERE id = ?`)
438
- .run(state, now, sessionId);
329
+ const chatDirs = scanChatDirs(this.sessionsDir);
330
+ for (const { dirPath } of chatDirs) {
331
+ const active = readJsonFile(path.join(dirPath, 'active.json'));
332
+ if (active && active.id === sessionId) {
333
+ active.activeTask = state;
334
+ active.updatedAt = now;
335
+ active.updatedAtStr = formatTimestamp(now);
336
+ atomicWriteJson(path.join(dirPath, 'active.json'), active);
337
+ return;
338
+ }
339
+ }
439
340
  }
440
- /** 从 processing_state 解析当前活跃 taskId */
441
341
  getActiveTaskId(sessionId) {
442
- const row = this.db.prepare(`SELECT processing_state FROM sessions WHERE id = ?`).get(sessionId);
443
- if (!row?.processing_state)
444
- return undefined;
445
- const colonIdx = row.processing_state.indexOf(':');
446
- return colonIdx > 0 ? row.processing_state.slice(colonIdx + 1) : undefined;
342
+ const chatDirs = scanChatDirs(this.sessionsDir);
343
+ for (const { dirPath } of chatDirs) {
344
+ const active = readJsonFile(path.join(dirPath, 'active.json'));
345
+ if (active && active.id === sessionId) {
346
+ if (!active.activeTask)
347
+ return undefined;
348
+ const colonIdx = active.activeTask.indexOf(':');
349
+ return colonIdx > 0 ? active.activeTask.slice(colonIdx + 1) : undefined;
350
+ }
351
+ }
352
+ return undefined;
447
353
  }
448
- /**
449
- * 清除会话处理中状态
450
- */
451
354
  clearProcessing(sessionId) {
452
- this.db.prepare(`UPDATE sessions SET processing_state = NULL, updated_at = ? WHERE id = ?`)
453
- .run(Date.now(), sessionId);
355
+ const now = Date.now();
356
+ const chatDirs = scanChatDirs(this.sessionsDir);
357
+ for (const { dirPath } of chatDirs) {
358
+ const active = readJsonFile(path.join(dirPath, 'active.json'));
359
+ if (active && active.id === sessionId) {
360
+ active.activeTask = null;
361
+ active.updatedAt = now;
362
+ active.updatedAtStr = formatTimestamp(now);
363
+ atomicWriteJson(path.join(dirPath, 'active.json'), active);
364
+ break;
365
+ }
366
+ }
454
367
  this.sessionEncryptState.delete(sessionId);
455
368
  }
456
369
  setSessionEncrypt(sessionId, encrypted) {
@@ -459,152 +372,140 @@ export class SessionManager {
459
372
  getSessionEncrypt(sessionId) {
460
373
  return this.sessionEncryptState.get(sessionId);
461
374
  }
462
- /**
463
- * 获取所有处于 processing 状态的会话(用于重启后恢复)
464
- * @param maxAgeMs 最大存活时间(超过则视为超时,清除状态)默认 1 小时
465
- */
466
375
  getPendingProcessingSessions(maxAgeMs = 60 * 60 * 1000) {
467
- const rows = this.db.prepare(`
468
- SELECT * FROM sessions
469
- WHERE processing_state IS NOT NULL AND deleted_at IS NULL
470
- `).all();
471
376
  const now = Date.now();
472
377
  const result = [];
473
- for (const row of rows) {
474
- const colonIdx = row.processing_state.indexOf(':');
475
- const ts = parseInt(colonIdx > 0 ? row.processing_state.slice(0, colonIdx) : row.processing_state, 10);
378
+ const chatDirs = scanChatDirs(this.sessionsDir);
379
+ for (const { dirPath } of chatDirs) {
380
+ const active = readJsonFile(path.join(dirPath, 'active.json'));
381
+ if (!active || !active.activeTask)
382
+ continue;
383
+ const colonIdx = active.activeTask.indexOf(':');
384
+ const ts = parseInt(colonIdx > 0 ? active.activeTask.slice(0, colonIdx) : active.activeTask, 10);
476
385
  if (!isNaN(ts) && (now - ts) < maxAgeMs) {
477
- result.push(this.rowToSession(row));
386
+ result.push(fileToSession(active));
478
387
  }
479
388
  else {
480
- // 超时:清除过期状态
481
- this.db.prepare(`UPDATE sessions SET processing_state = NULL WHERE id = ?`)
482
- .run(row.id);
389
+ active.activeTask = null;
390
+ active.updatedAt = now;
391
+ active.updatedAtStr = formatTimestamp(now);
392
+ atomicWriteJson(path.join(dirPath, 'active.json'), active);
483
393
  }
484
394
  }
485
395
  return result;
486
396
  }
487
- async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, agentId) {
488
- // 话题会话:独立查找/创建
397
+ // ─── Session lifecycle ───
398
+ async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, agentId, selfId, channelType, peerType) {
489
399
  if (threadId) {
490
- const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId);
400
+ const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType, peerType);
491
401
  session.identity = this.resolveIdentity(channel, userId);
492
- // 新话题会话补写默认权限模式
493
402
  if (session.metadata && !session.metadata.permissionMode) {
494
403
  session.metadata.permissionMode = DEFAULT_PERMISSION_MODE;
495
- this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
496
- .run(JSON.stringify(session.metadata), Date.now(), session.id);
404
+ this.appendMeta(channel, channelId, session);
497
405
  }
498
406
  return session;
499
407
  }
500
- // 主会话:查找活跃会话
501
- const active = this.db.prepare(`
502
- SELECT * FROM sessions
503
- WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true AND thread_id = '' AND deleted_at IS NULL
504
- `).get(channel, channelId);
505
- if (active) {
408
+ // 使用精确路径解析(caller 提供了 channelType 时直接定位,避免扫描回落)
409
+ const exactDir = this.resolveChatDirExact(channel, channelId, channelType, selfId);
410
+ const activeFile = readJsonFile(path.join(exactDir, 'active.json'));
411
+ const active = activeFile ? fileToSession(activeFile) : undefined;
412
+ if (active && !active.threadId) {
506
413
  const validSessionId = this.validateSessionFile(active);
507
- const session = { ...this.rowToSession(active), agentSessionId: validSessionId };
414
+ const session = { ...active, agentSessionId: validSessionId };
508
415
  session.identity = this.resolveIdentity(channel, userId);
509
- // chatType 自动修正:入站 chatType 与存储值不一致时更新(修复历史 session 因错误识别留下的脏数据)
416
+ let mutated = false;
510
417
  if (chatType && session.chatType !== chatType) {
511
418
  logger.info(`[SessionManager] Updating chatType for session ${session.id}: ${session.chatType} -> ${chatType}`);
512
- this.db.prepare(`UPDATE sessions SET chat_type = ?, updated_at = ? WHERE id = ?`)
513
- .run(chatType, Date.now(), active.id);
514
419
  session.chatType = chatType;
420
+ mutated = true;
421
+ }
422
+ if (selfId && session.selfId !== selfId) {
423
+ session.selfId = selfId;
424
+ mutated = true;
515
425
  }
516
- // 补写 peerId/peerName/channelName(旧 session 可能在这些字段引入前创建)
517
426
  if (chatType === 'private' && userId) {
518
- const activeMeta = active.metadata ? JSON.parse(active.metadata) : {};
519
- let updated = false;
520
- if (!activeMeta.peerId) {
521
- activeMeta.peerId = userId;
522
- updated = true;
427
+ if (!session.metadata)
428
+ session.metadata = {};
429
+ if (!session.metadata.peerId) {
430
+ session.metadata.peerId = userId;
431
+ mutated = true;
523
432
  }
524
- if (!activeMeta.peerName && metadata?.peerName) {
525
- activeMeta.peerName = metadata.peerName;
526
- updated = true;
433
+ if (!session.metadata.peerName && metadata?.peerName) {
434
+ session.metadata.peerName = metadata.peerName;
435
+ mutated = true;
527
436
  }
528
- if (metadata?.channelName && activeMeta.channelName !== metadata.channelName) {
529
- activeMeta.channelName = metadata.channelName;
530
- updated = true;
531
- }
532
- if (updated) {
533
- this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
534
- .run(JSON.stringify(activeMeta), Date.now(), active.id);
535
- session.metadata = activeMeta;
437
+ if (metadata?.channelName && session.metadata.channelName !== metadata.channelName) {
438
+ session.metadata.channelName = metadata.channelName;
439
+ mutated = true;
536
440
  }
537
441
  }
538
- // 补写 channelName(非私聊时也需要)
539
442
  if (metadata?.channelName && chatType !== 'private') {
540
- const activeMeta = active.metadata ? JSON.parse(active.metadata) : {};
541
- if (activeMeta.channelName !== metadata.channelName) {
542
- activeMeta.channelName = metadata.channelName;
543
- this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
544
- .run(JSON.stringify(activeMeta), Date.now(), active.id);
545
- session.metadata = activeMeta;
443
+ if (!session.metadata)
444
+ session.metadata = {};
445
+ if (session.metadata.channelName !== metadata.channelName) {
446
+ session.metadata.channelName = metadata.channelName;
447
+ mutated = true;
546
448
  }
547
449
  }
450
+ if (mutated) {
451
+ session.updatedAt = Date.now();
452
+ this.appendMeta(channel, channelId, session);
453
+ this.writeActive(channel, channelId, session);
454
+ }
548
455
  return session;
549
456
  }
550
- // 查找默认项目的主会话
551
- const existing = this.db.prepare(`
552
- SELECT * FROM sessions
553
- WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = '' AND deleted_at IS NULL
554
- ORDER BY updated_at DESC LIMIT 1
555
- `).get(channel, channelId, defaultProjectPath);
457
+ // Find existing session for default project path
458
+ const chatDir = this.resolveChatDir(channel, channelId);
459
+ const allSessions = this.findAllSessionsInChat(chatDir, false);
460
+ const existing = allSessions
461
+ .filter(s => s.projectPath === defaultProjectPath && !s.threadId)
462
+ .sort((a, b) => b.updatedAt - a.updatedAt)[0];
556
463
  if (existing) {
557
464
  const validSessionId = this.validateSessionFile(existing);
558
- // 激活此会话
559
- const existingMeta = existing.metadata ? JSON.parse(existing.metadata) : {};
560
- existingMeta.isActive = true;
561
- // chatType 自动修正(同 active 分支)
562
- const shouldUpdateChatType = chatType !== undefined && existing.chat_type !== chatType;
563
- if (shouldUpdateChatType) {
564
- logger.info(`[SessionManager] Updating chatType for session ${existing.id}: ${existing.chat_type} -> ${chatType}`);
565
- existing.chat_type = chatType;
566
- }
567
- // 补写 peerId/peerName
568
- if (chatType === 'private' && userId && !existingMeta.peerId) {
569
- existingMeta.peerId = userId;
465
+ const prev = JSON.parse(JSON.stringify({ ...existing, agentSessionId: validSessionId }));
466
+ const session = { ...existing, agentSessionId: validSessionId };
467
+ session.identity = this.resolveIdentity(channel, userId);
468
+ if (!session.metadata)
469
+ session.metadata = {};
470
+ if (selfId && session.selfId !== selfId) {
471
+ session.selfId = selfId;
570
472
  }
571
- if (chatType === 'private' && metadata?.peerName && !existingMeta.peerName) {
572
- existingMeta.peerName = metadata.peerName;
473
+ if (chatType && session.chatType !== chatType) {
474
+ logger.info(`[SessionManager] Updating chatType for session ${session.id}: ${session.chatType} -> ${chatType}`);
475
+ session.chatType = chatType;
573
476
  }
574
- if (shouldUpdateChatType) {
575
- this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ?, chat_type = ? WHERE id = ?`)
576
- .run(JSON.stringify(existingMeta), Date.now(), chatType, existing.id);
477
+ if (chatType === 'private' && userId && !session.metadata.peerId) {
478
+ session.metadata.peerId = userId;
577
479
  }
578
- else {
579
- this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
580
- .run(JSON.stringify(existingMeta), Date.now(), existing.id);
480
+ if (chatType === 'private' && metadata?.peerName && !session.metadata.peerName) {
481
+ session.metadata.peerName = metadata.peerName;
581
482
  }
582
- const session = { ...this.rowToSession(existing), agentSessionId: validSessionId, metadata: existingMeta };
583
- session.identity = this.resolveIdentity(channel, userId);
483
+ this.writeSessionIfChanged(channel, channelId, prev, session);
584
484
  return session;
585
485
  }
586
- // 创建新主会话
587
- const sessionMetadata = { ...metadata, isActive: true };
486
+ // Create new session
487
+ const sessionMetadata = { ...(metadata || {}) };
488
+ if (!sessionMetadata.permissionMode)
489
+ sessionMetadata.permissionMode = DEFAULT_PERMISSION_MODE;
588
490
  const session = {
589
- id: `${channel}-${channelId}-${Date.now()}`,
491
+ id: generateSessionId(),
590
492
  channel,
493
+ channelType: channelType || channel,
591
494
  channelId,
495
+ selfId,
592
496
  projectPath: defaultProjectPath,
593
497
  threadId: '',
594
498
  agentId: agentId || 'claude',
595
499
  chatType: chatType || 'private',
596
- sessionMode: this.resolveDefaultSessionMode(channel, chatType || 'private'),
500
+ sessionMode: this.resolveDefaultSessionMode(channel, chatType || 'private', peerType),
597
501
  metadata: sessionMetadata,
598
502
  name: name || '默认会话',
599
503
  createdAt: Date.now(),
600
- updatedAt: Date.now()
504
+ updatedAt: Date.now(),
601
505
  };
602
506
  session.identity = this.resolveIdentity(channel, userId);
603
- // 写入默认权限模式(统一 bypass,只在首次创建时设置)
604
- if (!sessionMetadata.permissionMode) {
605
- sessionMetadata.permissionMode = DEFAULT_PERMISSION_MODE;
606
- }
607
- this.insertSession(session);
507
+ this.appendMeta(channel, channelId, session);
508
+ this.writeActive(channel, channelId, session);
608
509
  this.eventBus.publish({
609
510
  type: 'session:created',
610
511
  sessionId: session.id,
@@ -613,81 +514,67 @@ export class SessionManager {
613
514
  projectPath: defaultProjectPath,
614
515
  name: session.name,
615
516
  chatType: session.chatType,
616
- timestamp: Date.now()
517
+ timestamp: Date.now(),
617
518
  });
618
519
  return session;
619
520
  }
620
521
  async updateSession(sessionId, updates) {
621
- const sets = [];
622
- const values = [];
623
- if (updates.chatType !== undefined) {
624
- sets.push('chat_type = ?');
625
- values.push(updates.chatType);
626
- }
627
- if (updates.name !== undefined) {
628
- sets.push('name = ?');
629
- values.push(updates.name);
630
- }
631
- if (updates.sessionMode !== undefined) {
632
- sets.push('session_mode = ?');
633
- values.push(updates.sessionMode);
634
- }
635
- if (updates.metadata !== undefined) {
636
- sets.push('metadata = ?');
637
- values.push(updates.metadata ? JSON.stringify(updates.metadata) : null);
638
- }
639
- if ('agentSessionId' in updates) {
640
- sets.push('claude_session_id = ?');
641
- values.push(updates.agentSessionId ?? null);
642
- }
643
- if (sets.length === 0)
522
+ const loaded = this.loadSessionForUpdate(sessionId);
523
+ if (!loaded)
644
524
  return;
645
- sets.push('updated_at = ?');
646
- values.push(Date.now());
647
- values.push(sessionId);
648
- this.db.prepare(`UPDATE sessions SET ${sets.join(', ')} WHERE id = ?`).run(...values);
649
- }
650
- getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId) {
651
- // 查找已有话题会话
652
- const existing = this.db.prepare(`
653
- SELECT * FROM sessions
654
- WHERE channel = ? AND channel_id = ? AND thread_id = ? AND deleted_at IS NULL
655
- `).get(channel, channelId, threadId);
656
- if (existing) {
657
- const validSessionId = this.validateSessionFile(existing);
658
- // 合并 metadata(如果提供)
659
- if (metadata) {
660
- const existingMeta = this.rowToSession(existing).metadata;
661
- const merged = existingMeta ? { ...existingMeta, ...metadata } : metadata;
662
- this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
663
- .run(JSON.stringify(merged), Date.now(), existing.id);
664
- return { ...this.rowToSession(existing), agentSessionId: validSessionId, metadata: merged };
525
+ const { current, prev } = loaded;
526
+ if (updates.chatType !== undefined)
527
+ current.chatType = updates.chatType;
528
+ if (updates.name !== undefined)
529
+ current.name = updates.name;
530
+ if (updates.sessionMode !== undefined)
531
+ current.sessionMode = updates.sessionMode;
532
+ if (updates.metadata !== undefined)
533
+ current.metadata = updates.metadata;
534
+ if ('agentSessionId' in updates)
535
+ current.agentSessionId = updates.agentSessionId ?? undefined;
536
+ this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
537
+ }
538
+ getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType, peerType) {
539
+ const chatDir = this.ensureResolvedChatDir(channel, channelId);
540
+ const threadIndex = readThreadIndex(chatDir);
541
+ const existingMetaId = threadIndex[threadId];
542
+ if (existingMetaId) {
543
+ const metaPath = path.join(chatDir, '_threads', `${existingMetaId}.jsonl`);
544
+ const existing = this.readMetaLatest(metaPath);
545
+ if (existing) {
546
+ const validSessionId = this.validateSessionFile(existing);
547
+ if (metadata) {
548
+ existing.metadata = { ...(existing.metadata || {}), ...metadata };
549
+ existing.updatedAt = Date.now();
550
+ this.appendMeta(channel, channelId, existing);
551
+ }
552
+ return { ...existing, agentSessionId: validSessionId };
665
553
  }
666
- return { ...this.rowToSession(existing), agentSessionId: validSessionId };
667
554
  }
668
- // 继承当前活跃主会话的项目路径和 chatType
669
- const activeMain = this.db.prepare(`
670
- SELECT project_path, chat_type FROM sessions
671
- WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true AND thread_id = ''
672
- `).get(channel, channelId);
673
- const projectPath = activeMain?.project_path || defaultProjectPath;
674
- // 创建新话题会话
675
- const inheritedChatType = activeMain?.chat_type || 'private';
555
+ // Inherit project path & chatType from active main session
556
+ const activeMain = this.readActive(channel, channelId);
557
+ const projectPath = (activeMain && !activeMain.threadId ? activeMain.projectPath : undefined) || defaultProjectPath;
558
+ const inheritedChatType = (activeMain && !activeMain.threadId ? activeMain.chatType : undefined) || 'private';
676
559
  const session = {
677
- id: `${channel}-${channelId}-${Date.now()}`,
560
+ id: generateSessionId(),
678
561
  channel,
562
+ channelType: channelType || channel,
679
563
  channelId,
564
+ selfId,
680
565
  projectPath,
681
566
  threadId,
682
567
  agentId: agentId || 'claude',
683
568
  chatType: inheritedChatType,
684
- sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
569
+ sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType, peerType),
685
570
  metadata,
686
571
  name: name || '话题会话',
687
572
  createdAt: Date.now(),
688
- updatedAt: Date.now()
573
+ updatedAt: Date.now(),
689
574
  };
690
- this.insertSession(session);
575
+ this.appendMeta(channel, channelId, session);
576
+ threadIndex[threadId] = session.id;
577
+ writeThreadIndex(chatDir, threadIndex);
691
578
  this.eventBus.publish({
692
579
  type: 'session:created',
693
580
  sessionId: session.id,
@@ -695,48 +582,45 @@ export class SessionManager {
695
582
  channelId,
696
583
  projectPath,
697
584
  name: session.name,
698
- timestamp: Date.now()
585
+ timestamp: Date.now(),
699
586
  });
700
587
  return session;
701
588
  }
702
589
  async switchProject(channel, channelId, newProjectPath, currentAgentId) {
703
590
  const agentId = currentAgentId || 'claude';
704
591
  logger.info(`[SessionManager] switchProject: channel=${channel} channelId=${channelId} newPath=${newProjectPath} agent=${agentId}`);
705
- // 1. 继承当前 chatType(在 deactivate 之前读取)
706
592
  const inheritedChatType = this.getActiveChatType(channel, channelId);
707
- // 2. 取消当前活跃会话
708
- this.deactivateAllMetadata(channel, channelId);
709
- // 3. 查找目标项目 + 当前 agent 的会话
710
- const target = this.db.prepare(`
711
- SELECT * FROM sessions
712
- WHERE channel = ? AND channel_id = ? AND project_path = ? AND agent_id = ? AND thread_id = '' AND deleted_at IS NULL
713
- ORDER BY updated_at DESC LIMIT 1
714
- `).get(channel, channelId, newProjectPath, agentId);
593
+ const chatDir = this.ensureResolvedChatDir(channel, channelId);
594
+ const allSessions = this.findAllSessionsInChat(chatDir, false);
595
+ const target = allSessions
596
+ .filter(s => s.projectPath === newProjectPath && (s.agentId || 'claude') === agentId && !s.threadId)
597
+ .sort((a, b) => b.updatedAt - a.updatedAt)[0];
715
598
  if (target) {
716
599
  const validSessionId = this.validateSessionFile(target);
717
- // 激活目标会话
718
- const metadata = target.metadata ? JSON.parse(target.metadata) : {};
719
- metadata.isActive = true;
720
- this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
721
- .run(JSON.stringify(metadata), Date.now(), target.id);
722
- return { ...this.rowToSession(target), agentSessionId: validSessionId, metadata };
600
+ target.agentSessionId = validSessionId;
601
+ target.updatedAt = Date.now();
602
+ this.appendMeta(channel, channelId, target);
603
+ this.writeActive(channel, channelId, target);
604
+ return target;
723
605
  }
724
- // 4. 创建新会话
725
606
  const session = {
726
- id: `${channel}-${channelId}-${Date.now()}`,
607
+ id: generateSessionId(),
727
608
  channel,
609
+ channelType: this.inferChannelType(channel, channelId),
728
610
  channelId,
611
+ selfId: this.inferSelfId(channel, channelId),
729
612
  projectPath: newProjectPath,
730
613
  threadId: '',
731
614
  agentId,
732
615
  chatType: inheritedChatType,
733
616
  sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
734
- metadata: { isActive: true },
617
+ metadata: {},
735
618
  name: '默认会话',
736
619
  createdAt: Date.now(),
737
- updatedAt: Date.now()
620
+ updatedAt: Date.now(),
738
621
  };
739
- this.insertSession(session);
622
+ this.appendMeta(channel, channelId, session);
623
+ this.writeActive(channel, channelId, session);
740
624
  this.eventBus.publish({
741
625
  type: 'session:created',
742
626
  sessionId: session.id,
@@ -744,63 +628,62 @@ export class SessionManager {
744
628
  channelId,
745
629
  projectPath: newProjectPath,
746
630
  name: session.name,
747
- timestamp: Date.now()
631
+ timestamp: Date.now(),
748
632
  });
749
633
  return session;
750
634
  }
751
635
  async updateAgentSessionId(channel, channelId, agentSessionId) {
752
- // 只更新当前活跃会话的 Agent Session ID
753
- this.db.prepare(`
754
- UPDATE sessions
755
- SET agent_session_id = ?, updated_at = ?
756
- WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true
757
- `).run(agentSessionId, Date.now(), channel, channelId);
636
+ const active = this.readActive(channel, channelId);
637
+ if (!active)
638
+ return;
639
+ const prev = JSON.parse(JSON.stringify(active));
640
+ active.agentSessionId = agentSessionId;
641
+ this.writeSessionIfChanged(channel, channelId, prev, active);
758
642
  }
759
643
  async updateAgentSessionIdBySessionId(sessionId, agentSessionId) {
760
- // 根据 sessionId 直接更新
761
- logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
762
- this.db.prepare(`
763
- UPDATE sessions
764
- SET agent_session_id = ?, updated_at = ?
765
- WHERE id = ?
766
- `).run(agentSessionId, Date.now(), sessionId);
644
+ const loaded = this.loadSessionForUpdate(sessionId);
645
+ if (!loaded)
646
+ return;
647
+ const { current, prev } = loaded;
648
+ current.agentSessionId = agentSessionId;
649
+ const wrote = this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
650
+ if (wrote) {
651
+ logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
652
+ }
767
653
  }
768
654
  async switchAgent(channel, channelId, projectPath, newAgentId) {
769
- // 1. 继承当前 chatType(在 deactivate 之前读取)
770
655
  const inheritedChatType = this.getActiveChatType(channel, channelId);
771
- // 2. 取消当前活跃会话
772
- this.deactivateAllMetadata(channel, channelId);
773
- // 3. 查找目标 agent 在当前项目下的会话
774
- const target = this.db.prepare(`
775
- SELECT * FROM sessions
776
- WHERE channel = ? AND channel_id = ? AND project_path = ? AND agent_id = ? AND thread_id = '' AND deleted_at IS NULL
777
- ORDER BY updated_at DESC LIMIT 1
778
- `).get(channel, channelId, projectPath, newAgentId);
656
+ const chatDir = this.ensureResolvedChatDir(channel, channelId);
657
+ const allSessions = this.findAllSessionsInChat(chatDir, false);
658
+ const target = allSessions
659
+ .filter(s => s.projectPath === projectPath && (s.agentId || 'claude') === newAgentId && !s.threadId)
660
+ .sort((a, b) => b.updatedAt - a.updatedAt)[0];
779
661
  if (target) {
780
662
  const validSessionId = this.validateSessionFile(target);
781
- // 激活目标会话
782
- const metadata = target.metadata ? JSON.parse(target.metadata) : {};
783
- metadata.isActive = true;
784
- this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
785
- .run(JSON.stringify(metadata), Date.now(), target.id);
786
- return { ...this.rowToSession(target), agentSessionId: validSessionId, metadata };
663
+ target.agentSessionId = validSessionId;
664
+ target.updatedAt = Date.now();
665
+ this.appendMeta(channel, channelId, target);
666
+ this.writeActive(channel, channelId, target);
667
+ return target;
787
668
  }
788
- // 4. 创建新会话(与 switchProject 保持一致)
789
669
  const session = {
790
- id: `${channel}-${channelId}-${Date.now()}`,
670
+ id: generateSessionId(),
791
671
  channel,
672
+ channelType: this.inferChannelType(channel, channelId),
792
673
  channelId,
674
+ selfId: this.inferSelfId(channel, channelId),
793
675
  projectPath,
794
676
  threadId: '',
795
677
  agentId: newAgentId,
796
678
  chatType: inheritedChatType,
797
679
  sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
798
- metadata: { isActive: true },
680
+ metadata: {},
799
681
  name: '默认会话',
800
682
  createdAt: Date.now(),
801
- updatedAt: Date.now()
683
+ updatedAt: Date.now(),
802
684
  };
803
- this.insertSession(session);
685
+ this.appendMeta(channel, channelId, session);
686
+ this.writeActive(channel, channelId, session);
804
687
  this.eventBus.publish({
805
688
  type: 'session:created',
806
689
  sessionId: session.id,
@@ -808,144 +691,229 @@ export class SessionManager {
808
691
  channelId,
809
692
  projectPath,
810
693
  name: session.name,
811
- timestamp: Date.now()
694
+ timestamp: Date.now(),
812
695
  });
813
696
  return session;
814
697
  }
815
698
  async clearActiveSession(channel, channelId) {
816
- // 清除当前活跃会话的 Agent Session ID
817
- this.db.prepare(`
818
- UPDATE sessions
819
- SET agent_session_id = NULL, updated_at = ?
820
- WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true
821
- `).run(Date.now(), channel, channelId);
822
- }
823
- /** 查找 owner 在目标通道的私聊 channelId(用于跨通道文件投递) */
699
+ const active = this.readActive(channel, channelId);
700
+ if (!active)
701
+ return;
702
+ const prev = JSON.parse(JSON.stringify(active));
703
+ active.agentSessionId = undefined;
704
+ this.writeSessionIfChanged(channel, channelId, prev, active);
705
+ }
824
706
  getOwnerChatId(targetChannel, ownerPeerId) {
825
- const row = this.db.prepare(`
826
- SELECT channel_id FROM sessions
827
- WHERE channel = ? AND chat_type = 'private'
828
- AND json_extract(metadata, '$.peerId') = ?
829
- AND deleted_at IS NULL
830
- ORDER BY updated_at DESC LIMIT 1
831
- `).get(targetChannel, ownerPeerId);
832
- return row?.channel_id;
707
+ const chatDirs = scanChatDirs(this.sessionsDir);
708
+ let bestMatch;
709
+ for (const { channelId, dirPath } of chatDirs) {
710
+ // Check active.json first
711
+ const active = readJsonFile(path.join(dirPath, 'active.json'));
712
+ // channel 实例名匹配(active.json 里有;缺失就跳过这个 chat)
713
+ if (!active || active.channel !== targetChannel)
714
+ continue;
715
+ const candidates = [active];
716
+ // Also scan meta files
717
+ for (const metaFile of scanMetaFiles(dirPath)) {
718
+ const file = readLastJsonlLine(path.join(dirPath, metaFile));
719
+ if (file)
720
+ candidates.push(file);
721
+ }
722
+ for (const cand of candidates) {
723
+ if (cand.chatType !== 'private')
724
+ continue;
725
+ if (cand.metadata?.peerId !== ownerPeerId)
726
+ continue;
727
+ if (!bestMatch || cand.updatedAt > bestMatch.updatedAt) {
728
+ bestMatch = { channelId, updatedAt: cand.updatedAt };
729
+ }
730
+ }
731
+ }
732
+ return bestMatch?.channelId;
833
733
  }
834
734
  async getSessionById(sessionId) {
835
- const row = this.db.prepare('SELECT * FROM sessions WHERE id = ? AND deleted_at IS NULL').get(sessionId);
836
- if (!row)
735
+ const found = this.findSessionFileById(sessionId);
736
+ if (!found)
837
737
  return undefined;
838
- return this.rowToSession(row);
738
+ return this.readMetaLatest(found.metaPath);
839
739
  }
840
740
  async getActiveSession(channel, channelId) {
841
- const row = this.db.prepare(`
842
- SELECT * FROM sessions
843
- WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true AND deleted_at IS NULL
844
- `).get(channel, channelId);
845
- if (!row)
846
- return undefined;
847
- return this.rowToSession(row);
741
+ return this.readActive(channel, channelId);
848
742
  }
849
- /**
850
- * 查询话题会话(不创建)
851
- */
852
743
  async getThreadSession(channel, channelId, threadId) {
853
- const row = this.db.prepare(`
854
- SELECT * FROM sessions
855
- WHERE channel = ? AND channel_id = ? AND thread_id = ? AND deleted_at IS NULL
856
- `).get(channel, channelId, threadId);
857
- if (!row)
744
+ const chatDir = this.resolveChatDir(channel, channelId);
745
+ const threadIndex = readThreadIndex(chatDir);
746
+ const metaId = threadIndex[threadId];
747
+ if (!metaId)
748
+ return undefined;
749
+ const metaPath = path.join(chatDir, '_threads', `${metaId}.jsonl`);
750
+ const session = this.readMetaLatest(metaPath);
751
+ if (!session)
858
752
  return undefined;
859
- const validSessionId = this.validateSessionFile(row);
860
- return { ...this.rowToSession(row), agentSessionId: validSessionId };
753
+ const validSessionId = this.validateSessionFile(session);
754
+ return { ...session, agentSessionId: validSessionId };
861
755
  }
862
756
  async listSessions(channel, channelId) {
863
- // 列出该聊天的所有会话
864
- const rows = this.db.prepare(`
865
- SELECT * FROM sessions
866
- WHERE channel = ? AND channel_id = ? AND deleted_at IS NULL
867
- ORDER BY updated_at DESC
868
- `).all(channel, channelId);
869
- return rows.map(row => this.rowToSession(row));
757
+ const chatDir = this.resolveChatDir(channel, channelId);
758
+ const sessions = this.findAllSessionsInChat(chatDir, true);
759
+ return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
870
760
  }
871
761
  async getSessionByProjectPath(channel, channelId, projectPath) {
872
- const row = this.db.prepare(`
873
- SELECT * FROM sessions
874
- WHERE channel = ? AND channel_id = ? AND project_path = ? AND deleted_at IS NULL
875
- ORDER BY processing_state IS NOT NULL DESC, updated_at DESC
876
- LIMIT 1
877
- `).get(channel, channelId, projectPath);
878
- if (!row)
762
+ const chatDir = this.resolveChatDir(channel, channelId);
763
+ const sessions = this.findAllSessionsInChat(chatDir, false);
764
+ const matched = sessions.filter(s => s.projectPath === projectPath);
765
+ if (matched.length === 0)
879
766
  return undefined;
880
- return this.rowToSession(row);
767
+ matched.sort((a, b) => {
768
+ const aProc = a.processingState ? 1 : 0;
769
+ const bProc = b.processingState ? 1 : 0;
770
+ if (aProc !== bProc)
771
+ return bProc - aProc;
772
+ return b.updatedAt - a.updatedAt;
773
+ });
774
+ return matched[0];
881
775
  }
882
776
  async getSessionByName(channel, channelId, name) {
883
- const row = this.db.prepare(`
884
- SELECT * FROM sessions
885
- WHERE channel = ? AND channel_id = ? AND name = ? AND deleted_at IS NULL
886
- `).get(channel, channelId, name);
887
- if (!row)
888
- return undefined;
889
- return this.rowToSession(row);
777
+ const chatDir = this.resolveChatDir(channel, channelId);
778
+ const sessions = this.findAllSessionsInChat(chatDir, true);
779
+ return sessions.find(s => s.name === name);
890
780
  }
891
781
  async switchToSession(channel, channelId, targetSessionId) {
892
- // 验证目标会话存在
893
- const target = this.db.prepare(`
894
- SELECT * FROM sessions WHERE id = ? AND channel = ? AND channel_id = ? AND deleted_at IS NULL
895
- `).get(targetSessionId, channel, channelId);
782
+ const chatDir = this.resolveChatDir(channel, channelId);
783
+ const sessions = this.findAllSessionsInChat(chatDir, true);
784
+ const target = sessions.find(s => s.id === targetSessionId);
896
785
  if (!target)
897
786
  return null;
898
- // 取消当前活跃会话
899
- this.deactivateAllMetadata(channel, channelId);
900
- // 激活目标会话
901
- const metadata = target.metadata ? JSON.parse(target.metadata) : {};
902
- metadata.isActive = true;
903
- this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
904
- .run(JSON.stringify(metadata), Date.now(), targetSessionId);
905
- return { ...this.rowToSession(target), metadata, updatedAt: Date.now() };
787
+ target.updatedAt = Date.now();
788
+ this.appendMeta(channel, channelId, target);
789
+ this.writeActive(channel, channelId, target);
790
+ return target;
906
791
  }
907
792
  updateMetadata(sessionId, metadata) {
908
- this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
909
- .run(JSON.stringify(metadata), Date.now(), sessionId);
793
+ const loaded = this.loadSessionForUpdate(sessionId);
794
+ if (!loaded)
795
+ return;
796
+ const { current, prev } = loaded;
797
+ current.metadata = metadata;
798
+ this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
910
799
  }
911
800
  async renameSession(sessionId, newName) {
912
- const result = this.db.prepare(`
913
- UPDATE sessions SET name = ?, updated_at = ? WHERE id = ?
914
- `).run(newName, Date.now(), sessionId);
915
- return result.changes > 0;
801
+ const loaded = this.loadSessionForUpdate(sessionId);
802
+ if (!loaded)
803
+ return false;
804
+ const { current, prev } = loaded;
805
+ current.name = newName;
806
+ this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
807
+ return true;
916
808
  }
917
809
  async unbindSession(sessionId) {
918
- const result = this.db.prepare(`
919
- DELETE FROM sessions WHERE id = ?
920
- `).run(sessionId);
921
- return result.changes > 0;
810
+ const found = this.findSessionFileById(sessionId);
811
+ if (!found)
812
+ return false;
813
+ const trashDir = path.join(found.chatDir, '_trash');
814
+ fs.mkdirSync(trashDir, { recursive: true });
815
+ const trashPath = path.join(trashDir, path.basename(found.metaPath));
816
+ try {
817
+ fs.renameSync(found.metaPath, trashPath);
818
+ }
819
+ catch (e) {
820
+ if (e.code !== 'ENOENT')
821
+ throw e;
822
+ }
823
+ // If thread session, remove from thread-index
824
+ if (found.isThread) {
825
+ const threadIndex = readThreadIndex(found.chatDir);
826
+ for (const [tid, mid] of Object.entries(threadIndex)) {
827
+ if (mid === sessionId) {
828
+ delete threadIndex[tid];
829
+ break;
830
+ }
831
+ }
832
+ writeThreadIndex(found.chatDir, threadIndex);
833
+ }
834
+ // Clear active.json if it pointed to this session
835
+ const activePath = path.join(found.chatDir, 'active.json');
836
+ const active = readJsonFile(activePath);
837
+ if (active && active.id === sessionId) {
838
+ try {
839
+ fs.unlinkSync(activePath);
840
+ }
841
+ catch (e) {
842
+ if (e.code !== 'ENOENT')
843
+ throw e;
844
+ }
845
+ }
846
+ return true;
922
847
  }
923
848
  async softDeleteSession(channelId) {
924
- this.db.prepare(`
925
- UPDATE sessions SET deleted_at = ?, updated_at = ? WHERE channel_id = ? AND deleted_at IS NULL
926
- `).run(Date.now(), Date.now(), channelId);
849
+ const chatDirs = scanChatDirs(this.sessionsDir);
850
+ for (const { channelId: cid, dirPath } of chatDirs) {
851
+ if (cid !== channelId)
852
+ continue;
853
+ const trashDir = path.join(dirPath, '_trash');
854
+ fs.mkdirSync(trashDir, { recursive: true });
855
+ for (const metaFile of scanMetaFiles(dirPath)) {
856
+ const src = path.join(dirPath, metaFile);
857
+ const dst = path.join(trashDir, metaFile);
858
+ try {
859
+ fs.renameSync(src, dst);
860
+ }
861
+ catch (e) {
862
+ if (e.code !== 'ENOENT')
863
+ throw e;
864
+ }
865
+ }
866
+ const threadsDir = path.join(dirPath, '_threads');
867
+ for (const metaFile of scanMetaFiles(threadsDir)) {
868
+ const src = path.join(threadsDir, metaFile);
869
+ const dst = path.join(trashDir, metaFile);
870
+ try {
871
+ fs.renameSync(src, dst);
872
+ }
873
+ catch (e) {
874
+ if (e.code !== 'ENOENT')
875
+ throw e;
876
+ }
877
+ }
878
+ // Clear active.json
879
+ const activePath = path.join(dirPath, 'active.json');
880
+ try {
881
+ fs.unlinkSync(activePath);
882
+ }
883
+ catch (e) {
884
+ if (e.code !== 'ENOENT')
885
+ throw e;
886
+ }
887
+ // Clear thread index
888
+ try {
889
+ fs.unlinkSync(path.join(threadsDir, 'thread-index.json'));
890
+ }
891
+ catch (e) {
892
+ if (e.code !== 'ENOENT')
893
+ throw e;
894
+ }
895
+ }
927
896
  }
928
897
  async createNewSession(channel, channelId, projectPath, name, agentId) {
929
- // 继承当前 chatType(在 deactivate 之前读取)
930
898
  const inheritedChatType = this.getActiveChatType(channel, channelId);
931
- // 取消当前活跃会话
932
- this.deactivateAllMetadata(channel, channelId);
933
- // 创建新会话
934
899
  const session = {
935
- id: `${channel}-${channelId}-${Date.now()}`,
900
+ id: generateSessionId(),
936
901
  channel,
902
+ channelType: this.inferChannelType(channel, channelId),
937
903
  channelId,
904
+ selfId: this.inferSelfId(channel, channelId),
938
905
  projectPath,
939
906
  threadId: '',
940
907
  agentId: agentId || 'claude',
941
908
  chatType: inheritedChatType,
942
909
  sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
943
- metadata: { isActive: true },
910
+ metadata: {},
944
911
  name: name || '默认会话',
945
912
  createdAt: Date.now(),
946
- updatedAt: Date.now()
913
+ updatedAt: Date.now(),
947
914
  };
948
- this.insertSession(session);
915
+ this.appendMeta(channel, channelId, session);
916
+ this.writeActive(channel, channelId, session);
949
917
  this.eventBus.publish({
950
918
  type: 'session:created',
951
919
  sessionId: session.id,
@@ -953,32 +921,30 @@ export class SessionManager {
953
921
  channelId,
954
922
  projectPath,
955
923
  name: session.name,
956
- timestamp: Date.now()
924
+ timestamp: Date.now(),
957
925
  });
958
926
  return session;
959
927
  }
960
- /**
961
- * 基于现有会话创建分支会话
962
- */
963
928
  async createForkedSession(sourceSession, forkedAgentSessionId, name) {
964
- // 取消当前活跃会话
965
- this.deactivateAllMetadata(sourceSession.channel, sourceSession.channelId);
966
929
  const session = {
967
- id: `${sourceSession.channel}-${sourceSession.channelId}-${Date.now()}`,
930
+ id: generateSessionId(),
968
931
  channel: sourceSession.channel,
932
+ channelType: sourceSession.channelType || sourceSession.channel,
969
933
  channelId: sourceSession.channelId,
934
+ selfId: sourceSession.selfId,
970
935
  projectPath: sourceSession.projectPath,
971
936
  threadId: sourceSession.threadId || '',
972
937
  agentId: sourceSession.agentId || 'claude',
973
938
  chatType: sourceSession.chatType || 'private',
974
939
  sessionMode: sourceSession.sessionMode || 'interactive',
975
940
  agentSessionId: forkedAgentSessionId,
976
- metadata: { isActive: true },
941
+ metadata: {},
977
942
  name: name || `${sourceSession.name || '会话'}-分支`,
978
943
  createdAt: Date.now(),
979
- updatedAt: Date.now()
944
+ updatedAt: Date.now(),
980
945
  };
981
- this.insertSession(session);
946
+ this.appendMeta(sourceSession.channel, sourceSession.channelId, session);
947
+ this.writeActive(sourceSession.channel, sourceSession.channelId, session);
982
948
  this.eventBus.publish({
983
949
  type: 'session:created',
984
950
  sessionId: session.id,
@@ -986,7 +952,7 @@ export class SessionManager {
986
952
  channelId: sourceSession.channelId,
987
953
  projectPath: sourceSession.projectPath,
988
954
  name: session.name,
989
- timestamp: Date.now()
955
+ timestamp: Date.now(),
990
956
  });
991
957
  return session;
992
958
  }
@@ -1014,18 +980,12 @@ export class SessionManager {
1014
980
  return null;
1015
981
  return adapter.readLastUserMessage(projectPath, agentSessionId);
1016
982
  }
1017
- /**
1018
- * 获取会话文件信息(回合数 + 标题)
1019
- */
1020
983
  getSessionFileInfo(projectPath, agentSessionId, agentId) {
1021
984
  const adapter = this.getFileAdapter(agentId);
1022
985
  if (!adapter)
1023
986
  return { turns: 0 };
1024
987
  return adapter.getFileInfo(projectPath, agentSessionId);
1025
988
  }
1026
- /**
1027
- * 列出 SDK 侧的会话列表(用于名称同步)
1028
- */
1029
989
  async listSdkSessions(projectPath, agentId) {
1030
990
  const adapter = this.getFileAdapter(agentId);
1031
991
  if (!adapter?.listSdkSessions)
@@ -1033,42 +993,39 @@ export class SessionManager {
1033
993
  return adapter.listSdkSessions(projectPath);
1034
994
  }
1035
995
  async getSessionByUuidPrefix(channel, channelId, uuidPrefix) {
1036
- const rows = this.db.prepare(`
1037
- SELECT * FROM sessions
1038
- WHERE channel = ? AND channel_id = ? AND agent_session_id LIKE ? AND deleted_at IS NULL
1039
- `).all(channel, channelId, `${uuidPrefix}%`);
1040
- if (rows.length === 0)
996
+ const chatDir = this.resolveChatDir(channel, channelId);
997
+ const sessions = this.findAllSessionsInChat(chatDir, true);
998
+ const matched = sessions.filter(s => s.agentSessionId && s.agentSessionId.startsWith(uuidPrefix));
999
+ if (matched.length === 0)
1041
1000
  return undefined;
1042
- if (rows.length > 1) {
1001
+ if (matched.length > 1) {
1043
1002
  logger.warn(`Multiple sessions found with UUID prefix: ${uuidPrefix}`);
1044
1003
  }
1045
- return this.rowToSession(rows[0]);
1004
+ return matched[0];
1046
1005
  }
1047
1006
  async importCliSession(channel, channelId, projectPath, agentSessionId, agentId = 'claude') {
1048
- // 继承当前 chatType(在 deactivate 之前读取)
1049
1007
  const inheritedChatType = this.getActiveChatType(channel, channelId);
1050
- // 取消当前活跃会话
1051
- this.deactivateAllMetadata(channel, channelId);
1052
- // 从 CLI 会话文件读取标题
1053
1008
  const fileInfo = this.getSessionFileInfo(projectPath, agentSessionId, agentId);
1054
1009
  const name = fileInfo.title || `CLI会话-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`;
1055
- // 创建新会话记录
1056
1010
  const session = {
1057
- id: `${channel}-${channelId}-${Date.now()}`,
1011
+ id: generateSessionId(),
1058
1012
  channel,
1013
+ channelType: this.inferChannelType(channel, channelId),
1059
1014
  channelId,
1015
+ selfId: this.inferSelfId(channel, channelId),
1060
1016
  projectPath,
1061
1017
  threadId: '',
1062
1018
  agentId,
1063
1019
  chatType: inheritedChatType,
1064
1020
  sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
1065
1021
  agentSessionId,
1066
- metadata: { isActive: true },
1022
+ metadata: {},
1067
1023
  name,
1068
1024
  createdAt: Date.now(),
1069
- updatedAt: Date.now()
1025
+ updatedAt: Date.now(),
1070
1026
  };
1071
- this.insertSession(session);
1027
+ this.appendMeta(channel, channelId, session);
1028
+ this.writeActive(channel, channelId, session);
1072
1029
  this.eventBus.publish({
1073
1030
  type: 'session:created',
1074
1031
  sessionId: session.id,
@@ -1076,111 +1033,117 @@ export class SessionManager {
1076
1033
  channelId,
1077
1034
  projectPath,
1078
1035
  name,
1079
- timestamp: Date.now()
1036
+ timestamp: Date.now(),
1080
1037
  });
1081
1038
  return session;
1082
1039
  }
1083
- // ==================== 健康状态管理 ====================
1084
- /**
1085
- * 获取会话健康状态
1086
- */
1040
+ // ─── Health status ───
1041
+ healthFilePath(channel, channelId) {
1042
+ return path.join(this.ensureResolvedChatDir(channel, channelId), 'health.jsonl');
1043
+ }
1044
+ /** Find the chat dir containing a given session id */
1045
+ chatDirForSession(sessionId) {
1046
+ const found = this.findSessionFileById(sessionId);
1047
+ if (!found)
1048
+ return undefined;
1049
+ // 从 meta jsonl 最后一行读 channel 实例名 + channelId
1050
+ const meta = readLastJsonlLine(found.metaPath);
1051
+ if (!meta)
1052
+ return undefined;
1053
+ return { channel: meta.channel, channelId: meta.channelId, dirPath: found.chatDir };
1054
+ }
1087
1055
  async getHealthStatus(sessionId) {
1088
- const row = this.db.prepare(`
1089
- SELECT * FROM session_health WHERE session_id = ?
1090
- `).get(sessionId);
1091
- if (!row) {
1092
- // 首次查询,创建默认记录
1093
- const now = Date.now();
1094
- this.db.prepare(`
1095
- INSERT INTO session_health (session_id, consecutive_errors, safe_mode, last_success_time, created_at, updated_at)
1096
- VALUES (?, 0, 0, ?, ?, ?)
1097
- `).run(sessionId, now, now, now);
1098
- return {
1099
- consecutiveErrors: 0,
1100
- safeMode: false,
1101
- lastSuccessTime: now
1102
- };
1056
+ const chatInfo = this.chatDirForSession(sessionId);
1057
+ if (!chatInfo) {
1058
+ return { consecutiveErrors: 0, safeMode: false, lastSuccessTime: Date.now() };
1103
1059
  }
1060
+ const healthPath = path.join(chatInfo.dirPath, 'health.jsonl');
1061
+ const records = readAllJsonlLines(healthPath).filter(r => r.sessionId === sessionId);
1062
+ let consecutiveErrors = 0;
1063
+ let lastError;
1064
+ let lastErrorType;
1065
+ let lastSuccessTime = 0;
1066
+ // Walk from tail to count consecutive errors
1067
+ for (let i = records.length - 1; i >= 0; i--) {
1068
+ const rec = records[i];
1069
+ if (rec.type === 'error') {
1070
+ consecutiveErrors++;
1071
+ if (lastError === undefined) {
1072
+ lastError = rec.error;
1073
+ lastErrorType = rec.errorType;
1074
+ }
1075
+ }
1076
+ else {
1077
+ // success or reset breaks the streak
1078
+ if (rec.type === 'success' && rec.at > lastSuccessTime)
1079
+ lastSuccessTime = rec.at;
1080
+ break;
1081
+ }
1082
+ }
1083
+ // Find most recent success time across all records
1084
+ if (lastSuccessTime === 0) {
1085
+ for (let i = records.length - 1; i >= 0; i--) {
1086
+ if (records[i].type === 'success' && records[i].at > lastSuccessTime) {
1087
+ lastSuccessTime = records[i].at;
1088
+ break;
1089
+ }
1090
+ }
1091
+ }
1092
+ if (lastSuccessTime === 0)
1093
+ lastSuccessTime = Date.now();
1104
1094
  return {
1105
- consecutiveErrors: row.consecutive_errors,
1106
- lastError: row.last_error,
1107
- lastErrorType: row.last_error_type,
1108
- safeMode: row.safe_mode === 1,
1109
- lastSuccessTime: row.last_success_time
1095
+ consecutiveErrors,
1096
+ lastError,
1097
+ lastErrorType,
1098
+ safeMode: false,
1099
+ lastSuccessTime,
1110
1100
  };
1111
1101
  }
1112
- /** 当前处于安全模式的会话数 */
1113
- getSafeModeSessionCount() {
1114
- const row = this.db.prepare(`SELECT COUNT(*) as count FROM session_health WHERE safe_mode = 1`).get();
1115
- return row?.count ?? 0;
1116
- }
1117
- /**
1118
- * 记录成功响应(重置错误计数)
1119
- */
1120
1102
  async recordSuccess(sessionId) {
1103
+ const chatInfo = this.chatDirForSession(sessionId);
1104
+ if (!chatInfo)
1105
+ return;
1121
1106
  const now = Date.now();
1122
- this.db.prepare(`
1123
- INSERT INTO session_health (session_id, consecutive_errors, safe_mode, last_success_time, created_at, updated_at)
1124
- VALUES (?, 0, 0, ?, ?, ?)
1125
- ON CONFLICT(session_id) DO UPDATE SET
1126
- consecutive_errors = 0,
1127
- last_error = NULL,
1128
- last_error_type = NULL,
1129
- last_success_time = ?,
1130
- updated_at = ?
1131
- `).run(sessionId, now, now, now, now, now);
1107
+ const record = {
1108
+ type: 'success',
1109
+ sessionId,
1110
+ at: now,
1111
+ atStr: formatTimestamp(now),
1112
+ };
1113
+ appendJsonl(path.join(chatInfo.dirPath, 'health.jsonl'), record);
1132
1114
  }
1133
- /**
1134
- * 记录错误(增加计数)
1135
- */
1136
1115
  async recordError(sessionId, errorType, errorMessage) {
1116
+ const chatInfo = this.chatDirForSession(sessionId);
1117
+ if (!chatInfo)
1118
+ return 0;
1137
1119
  const now = Date.now();
1138
- const health = await this.getHealthStatus(sessionId);
1139
- const newCount = health.consecutiveErrors + 1;
1140
- this.db.prepare(`
1141
- INSERT INTO session_health (session_id, consecutive_errors, last_error, last_error_type, safe_mode, last_success_time, created_at, updated_at)
1142
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1143
- ON CONFLICT(session_id) DO UPDATE SET
1144
- consecutive_errors = consecutive_errors + 1,
1145
- last_error = ?,
1146
- last_error_type = ?,
1147
- updated_at = ?
1148
- `).run(sessionId, newCount, errorMessage, errorType, health.safeMode ? 1 : 0, health.lastSuccessTime, now, now, errorMessage, errorType, now);
1149
- return newCount;
1150
- }
1151
- /**
1152
- * 设置安全模式
1153
- */
1154
- async setSafeMode(sessionId, enabled) {
1155
- const now = Date.now();
1156
- const health = await this.getHealthStatus(sessionId);
1157
- this.db.prepare(`
1158
- INSERT INTO session_health (session_id, consecutive_errors, safe_mode, last_success_time, created_at, updated_at)
1159
- VALUES (?, ?, ?, ?, ?, ?)
1160
- ON CONFLICT(session_id) DO UPDATE SET
1161
- safe_mode = ?,
1162
- updated_at = ?
1163
- `).run(sessionId, health.consecutiveErrors, enabled ? 1 : 0, health.lastSuccessTime, now, now, enabled ? 1 : 0, now);
1120
+ const record = {
1121
+ type: 'error',
1122
+ sessionId,
1123
+ errorType,
1124
+ error: errorMessage,
1125
+ at: now,
1126
+ atStr: formatTimestamp(now),
1127
+ };
1128
+ appendJsonl(path.join(chatInfo.dirPath, 'health.jsonl'), record);
1129
+ const status = await this.getHealthStatus(sessionId);
1130
+ return status.consecutiveErrors;
1164
1131
  }
1165
- /**
1166
- * 重置健康状态(用于修复后)
1167
- */
1168
1132
  async resetHealthStatus(sessionId) {
1133
+ const chatInfo = this.chatDirForSession(sessionId);
1134
+ if (!chatInfo)
1135
+ return;
1169
1136
  const now = Date.now();
1170
- this.db.prepare(`
1171
- INSERT INTO session_health (session_id, consecutive_errors, safe_mode, last_success_time, created_at, updated_at)
1172
- VALUES (?, 0, 0, ?, ?, ?)
1173
- ON CONFLICT(session_id) DO UPDATE SET
1174
- consecutive_errors = 0,
1175
- last_error = NULL,
1176
- last_error_type = NULL,
1177
- safe_mode = 0,
1178
- updated_at = ?
1179
- `).run(sessionId, now, now, now, now);
1137
+ const record = {
1138
+ type: 'reset',
1139
+ sessionId,
1140
+ at: now,
1141
+ atStr: formatTimestamp(now),
1142
+ };
1143
+ appendJsonl(path.join(chatInfo.dirPath, 'health.jsonl'), record);
1180
1144
  }
1181
1145
  close() {
1182
1146
  for (const adapter of this.fileAdapters.values())
1183
1147
  adapter.close?.();
1184
- this.db.close();
1185
1148
  }
1186
1149
  }