@wu529778790/open-im 1.11.1-beta.5 → 1.11.1-beta.7

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.
package/README.md CHANGED
@@ -48,10 +48,10 @@ After `start`, the CLI prints the dashboard URL (default **`http://127.0.0.1:392
48
48
  | --- | --- |
49
49
  | `/help` | Help |
50
50
  | `/new` | New AI session |
51
- | `/sessions` | Session history |
52
- | `/resume <N>` | Resume by list number |
51
+ | `/sessions` | Session history with preview |
52
+ | `/resume [N]` | Resume session (no arg = most recent) |
53
53
  | `/status` | AI + session info |
54
- | `/cd` / `/pwd` | Working directory |
54
+ | `/cd` / `/pwd` | Switch work dir (auto-resumes that dir's session) |
55
55
  | `/allow` / `/y`, `/deny` / `/n` | Permission prompts |
56
56
 
57
57
  ## Session continuity
package/README.zh-CN.md CHANGED
@@ -48,10 +48,10 @@ npx @wu529778790/open-im start
48
48
  | --- | --- |
49
49
  | `/help` | 帮助 |
50
50
  | `/new` | 新 AI 会话 |
51
- | `/sessions` | 历史会话 |
52
- | `/resume <序号>` | 按列表序号恢复 |
51
+ | `/sessions` | 历史会话(含摘要预览) |
52
+ | `/resume [序号]` | 恢复会话(无参数恢复最近一条) |
53
53
  | `/status` | AI 与会话信息 |
54
- | `/cd` / `/pwd` | 工作目录 |
54
+ | `/cd` / `/pwd` | 切换工作目录(自动恢复该目录的历史会话) |
55
55
  | `/allow` / `/y`、`/deny` / `/n` | 权限确认 |
56
56
 
57
57
  ## 会话接力
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
10
10
  */
11
+ import type { SDKSessionInfo } from '@anthropic-ai/claude-agent-sdk';
11
12
  import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
12
13
  interface ClaudeSessionMeta {
13
14
  sessionId: string;
@@ -16,11 +17,10 @@ interface ClaudeSessionMeta {
16
17
  size: number;
17
18
  }
18
19
  /**
19
- * 扫描 ~/.claude/projects/<encoded-path>/ 找到最新的 CLI session
20
- * 只返回 JSONL 文件(排除子目录如 subagents/),按修改时间倒序。
21
- * @param homeOverride 测试用:覆盖 homedir() 的返回值
20
+ * Find the latest CLI session for a work directory using the SDK's listSessions.
21
+ * Falls back to undefined if no sessions found.
22
22
  */
23
- export declare function findLatestClaudeSession(workDir: string, homeOverride?: string): ClaudeSessionMeta | undefined;
23
+ export declare function findLatestClaudeSession(workDir: string): Promise<ClaudeSessionMeta | undefined>;
24
24
  /**
25
25
  * 由 initAdapters 根据配置调用。ttlMinutes≤0 时关闭空闲回收(仍受 MAX_ACTIVE_SESSIONS 限制)。
26
26
  */
@@ -36,6 +36,11 @@ export declare class ClaudeSDKAdapter implements ToolAdapter {
36
36
  * Useful when the caller knows a session is corrupted.
37
37
  */
38
38
  static removeSession(sessionId: string): void;
39
+ /**
40
+ * List sessions for a directory using the SDK's listSessions API.
41
+ * Replaces the custom file-scanning logic in findLatestClaudeSession.
42
+ */
43
+ static listSessionsForDir(workDir: string, limit?: number): Promise<SDKSessionInfo[]>;
39
44
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
40
45
  }
41
46
  export {};
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
10
10
  */
11
- import { unstable_v2_createSession, unstable_v2_resumeSession } from '@anthropic-ai/claude-agent-sdk';
12
- import { existsSync, readFileSync, readdirSync, statSync, openSync, readSync, closeSync } from 'fs';
11
+ import { unstable_v2_createSession, unstable_v2_resumeSession, listSessions } from '@anthropic-ai/claude-agent-sdk';
12
+ import { existsSync, readFileSync, statSync, openSync, readSync, closeSync } from 'fs';
13
13
  import { execSync } from 'child_process';
14
14
  import { homedir } from 'os';
15
15
  import { join } from 'path';
@@ -99,51 +99,30 @@ function isCliSessionActive(sessionId, sessionFilePath) {
99
99
  }
100
100
  }
101
101
  /**
102
- * 扫描 ~/.claude/projects/<encoded-path>/ 找到最新的 CLI session
103
- * 只返回 JSONL 文件(排除子目录如 subagents/),按修改时间倒序。
104
- * @param homeOverride 测试用:覆盖 homedir() 的返回值
102
+ * Find the latest CLI session for a work directory using the SDK's listSessions.
103
+ * Falls back to undefined if no sessions found.
105
104
  */
106
- export function findLatestClaudeSession(workDir, homeOverride) {
107
- const projectDir = join(homeOverride ?? homedir(), '.claude', 'projects', workDirToProjectPath(workDir));
108
- if (!existsSync(projectDir)) {
109
- log.info(`No Claude Code project dir found: ${projectDir}`);
110
- return undefined;
111
- }
105
+ export async function findLatestClaudeSession(workDir) {
112
106
  try {
113
- const entries = readdirSync(projectDir, { withFileTypes: true });
114
- const sessions = [];
115
- for (const entry of entries) {
116
- // 只处理 .jsonl 文件,跳过子目录(如 subagents/)和 memory 目录
117
- if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
118
- continue;
119
- const sessionId = entry.name.replace('.jsonl', '');
120
- // 校验 UUID 格式
121
- if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(sessionId))
122
- continue;
123
- const filePath = join(projectDir, entry.name);
124
- try {
125
- const stat = statSync(filePath);
126
- // 跳过空文件
127
- if (stat.size === 0)
128
- continue;
129
- sessions.push({ sessionId, mtime: stat.mtimeMs, filePath, size: stat.size });
130
- }
131
- catch {
132
- continue;
133
- }
134
- }
107
+ const sessions = await listSessions({ dir: workDir, limit: 1 });
135
108
  if (sessions.length === 0) {
136
- log.info(`No session files found in ${projectDir}`);
109
+ log.info(`No sessions found for ${workDir}`);
137
110
  return undefined;
138
111
  }
139
- // 按修改时间倒序,最新的在前
140
- sessions.sort((a, b) => b.mtime - a.mtime);
141
112
  const latest = sessions[0];
142
- log.info(`Found latest Claude Code session: ${latest.sessionId} (mtime: ${new Date(latest.mtime).toISOString()}, size: ${latest.size})`);
143
- return latest;
113
+ // filePath is needed for isCliSessionActive check derive from standard path
114
+ const projectDir = join(homedir(), '.claude', 'projects', workDirToProjectPath(workDir));
115
+ const filePath = join(projectDir, `${latest.sessionId}.jsonl`);
116
+ log.info(`Found latest session via SDK: ${latest.sessionId} (lastModified: ${new Date(latest.lastModified).toISOString()})`);
117
+ return {
118
+ sessionId: latest.sessionId,
119
+ mtime: latest.lastModified,
120
+ filePath,
121
+ size: latest.fileSize ?? 0,
122
+ };
144
123
  }
145
124
  catch (err) {
146
- log.warn(`Failed to scan Claude Code project dir: ${err}`);
125
+ log.warn(`Failed to list sessions via SDK: ${err}`);
147
126
  return undefined;
148
127
  }
149
128
  }
@@ -345,7 +324,7 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode, onS
345
324
  // NOTE: process.chdir() 是进程级全局副作用,在并发服务器中不理想。
346
325
  // 但 SDK 的 createSession/resumeSession 不接受 cwd 参数,且这些调用是同步的,
347
326
  // 所以 mutex + try/finally 已是最优方案。如果 SDK 未来支持 cwd 选项,应移除 chdir。
348
- return withChdirMutex(() => {
327
+ return withChdirMutex(async () => {
349
328
  let session;
350
329
  const originalCwd = process.cwd();
351
330
  try {
@@ -380,7 +359,7 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode, onS
380
359
  // 没有指定 sessionId 时,尝试自动恢复 Claude Code CLI 的最新 session
381
360
  // 实现手机/电脑无缝切换:同目录下默认共享同一个对话
382
361
  if (!sessionId) {
383
- const latest = findLatestClaudeSession(workDir);
362
+ const latest = await findLatestClaudeSession(workDir);
384
363
  if (latest) {
385
364
  // 检测 CLI 是否正在使用该 session(用于日志,不阻止 resume)
386
365
  const cliActive = isCliSessionActive(latest.sessionId, latest.filePath);
@@ -472,6 +451,19 @@ export class ClaudeSDKAdapter {
472
451
  log.info(`Explicitly removed session: ${sessionId}`);
473
452
  }
474
453
  }
454
+ /**
455
+ * List sessions for a directory using the SDK's listSessions API.
456
+ * Replaces the custom file-scanning logic in findLatestClaudeSession.
457
+ */
458
+ static async listSessionsForDir(workDir, limit = 20) {
459
+ try {
460
+ return await listSessions({ dir: workDir, limit });
461
+ }
462
+ catch (err) {
463
+ log.warn(`Failed to list sessions for ${workDir}: ${err}`);
464
+ return [];
465
+ }
466
+ }
475
467
  run(prompt, sessionId, workDir, callbacks, options) {
476
468
  log.info(`[V2] run() entry model=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
477
469
  const abortController = new AbortController();
@@ -2,7 +2,30 @@ import { resolvePlatformAiCommand } from '../config.js';
2
2
  import { escapePathForMarkdown } from '../shared/utils.js';
3
3
  import { TERMINAL_ONLY_COMMANDS } from '../constants.js';
4
4
  import { createLogger } from '../logger.js';
5
+ import { ClaudeSDKAdapter } from '../adapters/claude-sdk-adapter.js';
5
6
  const log = createLogger('Commands');
7
+ function formatRelativeTime(ts) {
8
+ const sec = Math.floor((Date.now() - ts) / 1000);
9
+ if (sec < 60)
10
+ return '刚刚';
11
+ const min = Math.floor(sec / 60);
12
+ if (min < 60)
13
+ return `${min}分钟前`;
14
+ const hr = Math.floor(min / 60);
15
+ if (hr < 24)
16
+ return `${hr}小时前`;
17
+ const day = Math.floor(hr / 24);
18
+ if (day === 1)
19
+ return '昨天';
20
+ if (day < 30)
21
+ return `${day}天前`;
22
+ return new Date(ts).toLocaleDateString('zh-CN');
23
+ }
24
+ function truncateSummary(session, maxLen = 30) {
25
+ const text = session.customTitle || session.summary || session.firstPrompt || '新会话';
26
+ const firstLine = text.split('\n')[0].trim();
27
+ return firstLine.length > maxLen ? firstLine.slice(0, maxLen) + '...' : firstLine;
28
+ }
6
29
  import { AsyncLocalStorage } from 'node:async_hooks';
7
30
  import { execFile } from 'node:child_process';
8
31
  import { readdirSync } from 'node:fs';
@@ -84,7 +107,7 @@ export class CommandHandler {
84
107
  '/help - 显示帮助',
85
108
  '/new - 开始新会话(AI 上下文重置)',
86
109
  '/sessions - 查看历史会话',
87
- '/resume <序号> - 恢复历史会话',
110
+ '/resume [序号] - 恢复历史会话(无参数恢复最近一条)',
88
111
  '/status - 显示状态',
89
112
  '/cd <路径> - 切换工作目录',
90
113
  '/pwd - 当前工作目录',
@@ -93,44 +116,52 @@ export class CommandHandler {
93
116
  return true;
94
117
  }
95
118
  async handleSessions(chatId, userId, _platform) {
96
- const history = this.deps.sessionManager.listConvHistory(userId);
97
- const active = this.deps.sessionManager.getActiveConvInfo(userId);
98
- if (history.length === 0 && !active) {
119
+ const workDir = this.deps.sessionManager.getWorkDir(userId);
120
+ const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
121
+ if (sessions.length === 0) {
99
122
  await this.replySender().sendTextReply(chatId, '📋 暂无会话记录。');
100
123
  return true;
101
124
  }
102
125
  const lines = ['📋 会话列表:', ''];
103
- history.forEach((entry, i) => {
104
- lines.push(`${i + 1}. ${entry.convId} · ${entry.totalTurns}轮`);
126
+ sessions.forEach((session, i) => {
127
+ const preview = truncateSummary(session);
128
+ const time = session.lastModified ? ` · ${formatRelativeTime(session.lastModified)}` : '';
129
+ lines.push(`${i + 1}. ${preview}${time}`);
105
130
  });
106
- if (active) {
107
- const num = history.length + 1;
108
- lines.push(`▸ ${num}. ${active.convId} · ${active.totalTurns}轮(当前)`);
109
- }
110
- lines.push('', '使用 /resume <序号> 恢复历史会话');
131
+ lines.push('', '使用 /resume <序号> 恢复,或 /resume 恢复最近一条');
111
132
  await this.replySender().sendTextReply(chatId, lines.join('\n'));
112
133
  return true;
113
134
  }
114
135
  async handleResume(chatId, userId, arg, _platform) {
136
+ const workDir = this.deps.sessionManager.getWorkDir(userId);
137
+ const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
138
+ // /resume (no arg) — resume the most recent session
139
+ if (!arg) {
140
+ if (sessions.length === 0) {
141
+ await this.replySender().sendTextReply(chatId, '没有可恢复的历史会话。');
142
+ return true;
143
+ }
144
+ const session = sessions[0];
145
+ this.deps.requestQueue.cancelUser(userId);
146
+ this.deps.sessionManager.setActiveSessionId(userId, session.sessionId);
147
+ const preview = truncateSummary(session);
148
+ await this.replySender().sendTextReply(chatId, `✅ 已恢复最近会话: ${preview}\n继续发消息即可。`);
149
+ return true;
150
+ }
115
151
  const index = parseInt(arg, 10);
116
152
  if (isNaN(index) || index < 1) {
117
- await this.replySender().sendTextReply(chatId, '用法: /resume <序号>\n\n使用 /sessions 查看会话列表。');
153
+ await this.replySender().sendTextReply(chatId, '用法: /resume [序号]\n\n不带序号则恢复最近一条会话。');
118
154
  return true;
119
155
  }
120
- const history = this.deps.sessionManager.listConvHistory(userId);
121
- if (index > history.length) {
122
- await this.replySender().sendTextReply(chatId, `序号 ${index} 无效,共 ${history.length} 个历史会话。`);
156
+ if (index > sessions.length) {
157
+ await this.replySender().sendTextReply(chatId, `序号 ${index} 无效,共 ${sessions.length} 个历史会话。`);
123
158
  return true;
124
159
  }
125
- const entry = history[index - 1];
160
+ const session = sessions[index - 1];
126
161
  this.deps.requestQueue.cancelUser(userId);
127
- const ok = this.deps.sessionManager.resumeConv(userId, entry.convId);
128
- if (ok) {
129
- await this.replySender().sendTextReply(chatId, `✅ 已恢复会话 ${index} (${entry.convId}),共 ${entry.totalTurns}轮对话。\n继续发消息即可。`);
130
- }
131
- else {
132
- await this.replySender().sendTextReply(chatId, '❌ 恢复会话失败,请重试。');
133
- }
162
+ this.deps.sessionManager.setActiveSessionId(userId, session.sessionId);
163
+ const preview = truncateSummary(session);
164
+ await this.replySender().sendTextReply(chatId, `✅ 已恢复会话: ${preview}\n继续发消息即可。`);
134
165
  return true;
135
166
  }
136
167
  async handleNew(chatId, userId) {
@@ -179,10 +210,7 @@ export class CommandHandler {
179
210
  try {
180
211
  this.deps.requestQueue.cancelUser(userId);
181
212
  const result = await this.deps.sessionManager.setWorkDir(userId, dir);
182
- const msg = result.resumed
183
- ? `📁 工作目录已切换到: ${escapePathForMarkdown(result.path)}\n\n🔄 已恢复该目录的最近会话,继续之前的上下文。`
184
- : `📁 工作目录已切换到: ${escapePathForMarkdown(result.path)}\n\n🆕 该目录暂无历史会话,已创建全新上下文。`;
185
- await this.replySender().sendTextReply(chatId, msg);
213
+ await this.replySender().sendTextReply(chatId, `📁 工作目录已切换到: ${escapePathForMarkdown(result.path)}\n\n下一条消息将自动查找该目录的最近会话。`);
186
214
  }
187
215
  catch (err) {
188
216
  await this.replySender().sendTextReply(chatId, err instanceof Error ? err.message : String(err));
@@ -1,10 +1,4 @@
1
1
  type ToolId = 'claude' | 'codex' | 'codebuddy' | 'opencode';
2
- interface ConvHistoryEntry {
3
- convId: string;
4
- totalTurns: number;
5
- createdAt: number;
6
- workDir?: string;
7
- }
8
2
  export declare function resolveWorkDirInput(baseDir: string, targetDir: string): string;
9
3
  export declare class SessionManager {
10
4
  private sessions;
@@ -30,13 +24,12 @@ export declare class SessionManager {
30
24
  resumed: boolean;
31
25
  }>;
32
26
  newSession(userId: string): boolean;
27
+ /**
28
+ * Set the active session to a specific SDK session (used by /resume).
29
+ * Bridges the SDK sessionId to our internal activeConvId tracking.
30
+ */
31
+ setActiveSessionId(userId: string, sessionId: string): void;
33
32
  clearActiveToolSession(userId: string, toolId: ToolId): boolean;
34
- listConvHistory(userId: string): ConvHistoryEntry[];
35
- getActiveConvInfo(userId: string): {
36
- convId: string;
37
- totalTurns: number;
38
- } | undefined;
39
- resumeConv(userId: string, convId: string): boolean;
40
33
  addTurns(userId: string, turns: number): number;
41
34
  addTurnsForThread(userId: string, threadId: string, turns: number): number;
42
35
  getModel(userId: string, threadId?: string): string | undefined;
@@ -107,52 +107,16 @@ export class SessionManager {
107
107
  const currentDir = this.getWorkDir(userId);
108
108
  const realPath = await this.resolveAndValidate(currentDir, workDir);
109
109
  const s = this.sessions.get(userId);
110
- let oldConvId;
111
- let resumed = false;
112
110
  if (s) {
113
111
  // Same directory — no-op
114
112
  if (realPath === currentDir) {
115
113
  return { path: realPath, resumed: false };
116
114
  }
117
- oldConvId = s.activeConvId;
118
115
  this.persistActiveConvSessions(userId, s);
119
- // Archive current conversation with its workDir
120
- if (oldConvId) {
121
- if (!s.convHistory)
122
- s.convHistory = [];
123
- s.convHistory.push({
124
- convId: oldConvId,
125
- totalTurns: s.totalTurns ?? 0,
126
- createdAt: Date.now(),
127
- workDir: currentDir,
128
- });
129
- if (s.convHistory.length > 10)
130
- s.convHistory = s.convHistory.slice(-10);
131
- }
132
- // Look for a previous conversation in the target directory
133
- if (!s.convHistory)
134
- s.convHistory = [];
135
- const matchIdx = s.convHistory.findIndex((e) => e.workDir === realPath);
136
- if (matchIdx !== -1) {
137
- const [entry] = s.convHistory.splice(matchIdx, 1);
138
- s.activeConvId = entry.convId;
139
- s.totalTurns = entry.totalTurns;
140
- s.sessionIds = {};
141
- for (const toolId of ['claude', 'codex', 'codebuddy', 'opencode']) {
142
- const sessionId = this.convSessionMap.get(this.getConvSessionKey(userId, entry.convId, toolId));
143
- if (sessionId) {
144
- if (!s.sessionIds)
145
- s.sessionIds = {};
146
- s.sessionIds[toolId] = sessionId;
147
- }
148
- }
149
- resumed = true;
150
- log.info(`Resumed session for user ${userId}: convId=${entry.convId}, workDir=${realPath}, turns=${entry.totalTurns}`);
151
- }
152
- else {
153
- s.sessionIds = {};
154
- s.activeConvId = randomBytes(4).toString('hex');
155
- }
116
+ // Clear active session IDs; SDK adapter will auto-resume latest CLI session for new dir
117
+ s.sessionIds = {};
118
+ s.activeConvId = randomBytes(4).toString('hex');
119
+ s.totalTurns = 0;
156
120
  s.workDir = realPath;
157
121
  }
158
122
  else {
@@ -162,8 +126,8 @@ export class SessionManager {
162
126
  });
163
127
  }
164
128
  this.flushSync();
165
- log.info(`WorkDir changed for user ${userId}: ${realPath}, oldConvId=${oldConvId}, resumed=${resumed}`);
166
- return { path: realPath, resumed };
129
+ log.info(`WorkDir changed for user ${userId}: ${realPath}`);
130
+ return { path: realPath, resumed: false };
167
131
  }
168
132
  newSession(userId) {
169
133
  const s = this.sessions.get(userId);
@@ -171,20 +135,6 @@ export class SessionManager {
171
135
  const oldSessionIds = { ...(s.sessionIds ?? {}) };
172
136
  const oldConvId = s.activeConvId;
173
137
  this.persistActiveConvSessions(userId, s);
174
- // Archive old conversation to history (keep convSessionMap entries for resume)
175
- if (oldConvId) {
176
- if (!s.convHistory)
177
- s.convHistory = [];
178
- s.convHistory.push({
179
- convId: oldConvId,
180
- totalTurns: s.totalTurns ?? 0,
181
- createdAt: Date.now(),
182
- workDir: s.workDir,
183
- });
184
- // Keep last 10 entries
185
- if (s.convHistory.length > 10)
186
- s.convHistory = s.convHistory.slice(-10);
187
- }
188
138
  s.sessionIds = {};
189
139
  s.activeConvId = randomBytes(4).toString('hex');
190
140
  s.totalTurns = 0;
@@ -194,6 +144,25 @@ export class SessionManager {
194
144
  }
195
145
  return false;
196
146
  }
147
+ /**
148
+ * Set the active session to a specific SDK session (used by /resume).
149
+ * Bridges the SDK sessionId to our internal activeConvId tracking.
150
+ */
151
+ setActiveSessionId(userId, sessionId) {
152
+ let s = this.sessions.get(userId);
153
+ if (!s) {
154
+ s = { workDir: this.defaultWorkDir };
155
+ this.sessions.set(userId, s);
156
+ }
157
+ this.persistActiveConvSessions(userId, s);
158
+ s.activeConvId = randomBytes(4).toString('hex');
159
+ s.totalTurns = 0;
160
+ if (!s.sessionIds)
161
+ s.sessionIds = {};
162
+ s.sessionIds.claude = sessionId;
163
+ this.flushSync();
164
+ log.info(`Set active session for user ${userId}: sessionId=${sessionId}, convId=${s.activeConvId}`);
165
+ }
197
166
  clearActiveToolSession(userId, toolId) {
198
167
  const s = this.sessions.get(userId);
199
168
  if (!s)
@@ -208,57 +177,6 @@ export class SessionManager {
208
177
  log.info(`Cleared active ${toolId} session for user ${userId}, convId=${activeConvId ?? 'none'}`);
209
178
  return hadSession;
210
179
  }
211
- listConvHistory(userId) {
212
- const s = this.sessions.get(userId);
213
- if (!s)
214
- return [];
215
- return s.convHistory ?? [];
216
- }
217
- getActiveConvInfo(userId) {
218
- const s = this.sessions.get(userId);
219
- if (!s?.activeConvId)
220
- return undefined;
221
- return { convId: s.activeConvId, totalTurns: s.totalTurns ?? 0 };
222
- }
223
- resumeConv(userId, convId) {
224
- const s = this.sessions.get(userId);
225
- if (!s)
226
- return false;
227
- // Find target in history
228
- const idx = s.convHistory?.findIndex((e) => e.convId === convId) ?? -1;
229
- if (idx === -1)
230
- return false;
231
- // Archive current active session
232
- this.persistActiveConvSessions(userId, s);
233
- if (s.activeConvId) {
234
- if (!s.convHistory)
235
- s.convHistory = [];
236
- s.convHistory.push({
237
- convId: s.activeConvId,
238
- totalTurns: s.totalTurns ?? 0,
239
- createdAt: Date.now(),
240
- workDir: s.workDir,
241
- });
242
- }
243
- // Remove target from history
244
- const [entry] = s.convHistory.splice(idx, 1);
245
- // Restore target as active
246
- s.activeConvId = entry.convId;
247
- s.totalTurns = entry.totalTurns;
248
- s.sessionIds = {};
249
- // Restore sessionIds from convSessionMap
250
- for (const toolId of ['claude', 'codex', 'codebuddy']) {
251
- const sessionId = this.convSessionMap.get(this.getConvSessionKey(userId, entry.convId, toolId));
252
- if (sessionId) {
253
- if (!s.sessionIds)
254
- s.sessionIds = {};
255
- s.sessionIds[toolId] = sessionId;
256
- }
257
- }
258
- this.flushSync();
259
- log.info(`Resumed session for user ${userId}: convId=${entry.convId}, turns=${entry.totalTurns}`);
260
- return true;
261
- }
262
180
  addTurns(userId, turns) {
263
181
  const s = this.sessions.get(userId);
264
182
  if (!s)
@@ -345,6 +263,9 @@ export class SessionManager {
345
263
  log.warn(`Legacy shared sessionId found for user ${k}; clearing it to avoid cross-tool resume conflicts`);
346
264
  }
347
265
  delete v.sessionId;
266
+ // Clean up legacy fields
267
+ delete v.firstMessage;
268
+ delete v.convHistory;
348
269
  if (v.threads) {
349
270
  for (const thread of Object.values(v.threads)) {
350
271
  if (!thread.sessionIds)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.11.1-beta.5",
3
+ "version": "1.11.1-beta.7",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",