@wu529778790/open-im 1.11.1-beta.6 → 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/dist/adapters/claude-sdk-adapter.d.ts +9 -4
- package/dist/adapters/claude-sdk-adapter.js +33 -41
- package/dist/commands/handler.js +25 -42
- package/dist/session/session-manager.d.ts +5 -16
- package/dist/session/session-manager.js +28 -121
- package/dist/shared/ai-task.js +0 -1
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
20
|
-
*
|
|
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
|
|
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,
|
|
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
|
-
*
|
|
103
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
143
|
-
|
|
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
|
|
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();
|
package/dist/commands/handler.js
CHANGED
|
@@ -2,6 +2,7 @@ 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');
|
|
6
7
|
function formatRelativeTime(ts) {
|
|
7
8
|
const sec = Math.floor((Date.now() - ts) / 1000);
|
|
@@ -20,10 +21,9 @@ function formatRelativeTime(ts) {
|
|
|
20
21
|
return `${day}天前`;
|
|
21
22
|
return new Date(ts).toLocaleDateString('zh-CN');
|
|
22
23
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const firstLine = msg.split('\n')[0].trim();
|
|
24
|
+
function truncateSummary(session, maxLen = 30) {
|
|
25
|
+
const text = session.customTitle || session.summary || session.firstPrompt || '新会话';
|
|
26
|
+
const firstLine = text.split('\n')[0].trim();
|
|
27
27
|
return firstLine.length > maxLen ? firstLine.slice(0, maxLen) + '...' : firstLine;
|
|
28
28
|
}
|
|
29
29
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
@@ -116,45 +116,36 @@ export class CommandHandler {
|
|
|
116
116
|
return true;
|
|
117
117
|
}
|
|
118
118
|
async handleSessions(chatId, userId, _platform) {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
119
|
+
const workDir = this.deps.sessionManager.getWorkDir(userId);
|
|
120
|
+
const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
|
|
121
|
+
if (sessions.length === 0) {
|
|
122
122
|
await this.replySender().sendTextReply(chatId, '📋 暂无会话记录。');
|
|
123
123
|
return true;
|
|
124
124
|
}
|
|
125
125
|
const lines = ['📋 会话列表:', ''];
|
|
126
|
-
|
|
127
|
-
const preview =
|
|
128
|
-
const time =
|
|
129
|
-
lines.push(`${i + 1}. ${preview}
|
|
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}`);
|
|
130
130
|
});
|
|
131
|
-
if (active) {
|
|
132
|
-
const num = history.length + 1;
|
|
133
|
-
const preview = truncatePreview(active.firstMessage);
|
|
134
|
-
lines.push(`▸ ${num}. ${preview} · ${active.totalTurns}轮(当前)`);
|
|
135
|
-
}
|
|
136
131
|
lines.push('', '使用 /resume <序号> 恢复,或 /resume 恢复最近一条');
|
|
137
132
|
await this.replySender().sendTextReply(chatId, lines.join('\n'));
|
|
138
133
|
return true;
|
|
139
134
|
}
|
|
140
135
|
async handleResume(chatId, userId, arg, _platform) {
|
|
141
|
-
const
|
|
136
|
+
const workDir = this.deps.sessionManager.getWorkDir(userId);
|
|
137
|
+
const sessions = await ClaudeSDKAdapter.listSessionsForDir(workDir);
|
|
142
138
|
// /resume (no arg) — resume the most recent session
|
|
143
139
|
if (!arg) {
|
|
144
|
-
if (
|
|
140
|
+
if (sessions.length === 0) {
|
|
145
141
|
await this.replySender().sendTextReply(chatId, '没有可恢复的历史会话。');
|
|
146
142
|
return true;
|
|
147
143
|
}
|
|
148
|
-
const
|
|
144
|
+
const session = sessions[0];
|
|
149
145
|
this.deps.requestQueue.cancelUser(userId);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
await this.replySender().sendTextReply(chatId, `✅ 已恢复最近会话: ${preview}(${entry.totalTurns}轮)\n继续发消息即可。`);
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
await this.replySender().sendTextReply(chatId, '❌ 恢复会话失败,请重试。');
|
|
157
|
-
}
|
|
146
|
+
this.deps.sessionManager.setActiveSessionId(userId, session.sessionId);
|
|
147
|
+
const preview = truncateSummary(session);
|
|
148
|
+
await this.replySender().sendTextReply(chatId, `✅ 已恢复最近会话: ${preview}\n继续发消息即可。`);
|
|
158
149
|
return true;
|
|
159
150
|
}
|
|
160
151
|
const index = parseInt(arg, 10);
|
|
@@ -162,20 +153,15 @@ export class CommandHandler {
|
|
|
162
153
|
await this.replySender().sendTextReply(chatId, '用法: /resume [序号]\n\n不带序号则恢复最近一条会话。');
|
|
163
154
|
return true;
|
|
164
155
|
}
|
|
165
|
-
if (index >
|
|
166
|
-
await this.replySender().sendTextReply(chatId, `序号 ${index} 无效,共 ${
|
|
156
|
+
if (index > sessions.length) {
|
|
157
|
+
await this.replySender().sendTextReply(chatId, `序号 ${index} 无效,共 ${sessions.length} 个历史会话。`);
|
|
167
158
|
return true;
|
|
168
159
|
}
|
|
169
|
-
const
|
|
160
|
+
const session = sessions[index - 1];
|
|
170
161
|
this.deps.requestQueue.cancelUser(userId);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
await this.replySender().sendTextReply(chatId, `✅ 已恢复会话: ${preview}(${entry.totalTurns}轮)\n继续发消息即可。`);
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
await this.replySender().sendTextReply(chatId, '❌ 恢复会话失败,请重试。');
|
|
178
|
-
}
|
|
162
|
+
this.deps.sessionManager.setActiveSessionId(userId, session.sessionId);
|
|
163
|
+
const preview = truncateSummary(session);
|
|
164
|
+
await this.replySender().sendTextReply(chatId, `✅ 已恢复会话: ${preview}\n继续发消息即可。`);
|
|
179
165
|
return true;
|
|
180
166
|
}
|
|
181
167
|
async handleNew(chatId, userId) {
|
|
@@ -224,10 +210,7 @@ export class CommandHandler {
|
|
|
224
210
|
try {
|
|
225
211
|
this.deps.requestQueue.cancelUser(userId);
|
|
226
212
|
const result = await this.deps.sessionManager.setWorkDir(userId, dir);
|
|
227
|
-
|
|
228
|
-
? `📁 工作目录已切换到: ${escapePathForMarkdown(result.path)}\n\n🔄 已恢复该目录的最近会话,继续之前的上下文。`
|
|
229
|
-
: `📁 工作目录已切换到: ${escapePathForMarkdown(result.path)}\n\n🆕 该目录暂无历史会话,已创建全新上下文。`;
|
|
230
|
-
await this.replySender().sendTextReply(chatId, msg);
|
|
213
|
+
await this.replySender().sendTextReply(chatId, `📁 工作目录已切换到: ${escapePathForMarkdown(result.path)}\n\n下一条消息将自动查找该目录的最近会话。`);
|
|
231
214
|
}
|
|
232
215
|
catch (err) {
|
|
233
216
|
await this.replySender().sendTextReply(chatId, err instanceof Error ? err.message : String(err));
|
|
@@ -1,11 +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
|
-
firstMessage?: string;
|
|
8
|
-
}
|
|
9
2
|
export declare function resolveWorkDirInput(baseDir: string, targetDir: string): string;
|
|
10
3
|
export declare class SessionManager {
|
|
11
4
|
private sessions;
|
|
@@ -31,18 +24,14 @@ export declare class SessionManager {
|
|
|
31
24
|
resumed: boolean;
|
|
32
25
|
}>;
|
|
33
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;
|
|
34
32
|
clearActiveToolSession(userId: string, toolId: ToolId): boolean;
|
|
35
|
-
listConvHistory(userId: string): ConvHistoryEntry[];
|
|
36
|
-
getActiveConvInfo(userId: string): {
|
|
37
|
-
convId: string;
|
|
38
|
-
totalTurns: number;
|
|
39
|
-
firstMessage?: string;
|
|
40
|
-
} | undefined;
|
|
41
|
-
resumeConv(userId: string, convId: string): boolean;
|
|
42
33
|
addTurns(userId: string, turns: number): number;
|
|
43
34
|
addTurnsForThread(userId: string, threadId: string, turns: number): number;
|
|
44
|
-
/** Save the first message preview for the current conversation (only on first turn). */
|
|
45
|
-
setConvPreview(userId: string, preview: string): void;
|
|
46
35
|
getModel(userId: string, threadId?: string): string | undefined;
|
|
47
36
|
setModel(userId: string, model: string | undefined, threadId?: string): void;
|
|
48
37
|
private resolveAndValidate;
|
|
@@ -107,54 +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
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
s.convHistory.push({
|
|
124
|
-
convId: oldConvId,
|
|
125
|
-
totalTurns: s.totalTurns ?? 0,
|
|
126
|
-
createdAt: Date.now(),
|
|
127
|
-
workDir: currentDir,
|
|
128
|
-
firstMessage: s.firstMessage,
|
|
129
|
-
});
|
|
130
|
-
if (s.convHistory.length > 10)
|
|
131
|
-
s.convHistory = s.convHistory.slice(-10);
|
|
132
|
-
}
|
|
133
|
-
// Look for a previous conversation in the target directory
|
|
134
|
-
if (!s.convHistory)
|
|
135
|
-
s.convHistory = [];
|
|
136
|
-
const matchIdx = s.convHistory.findIndex((e) => e.workDir === realPath);
|
|
137
|
-
if (matchIdx !== -1) {
|
|
138
|
-
const [entry] = s.convHistory.splice(matchIdx, 1);
|
|
139
|
-
s.activeConvId = entry.convId;
|
|
140
|
-
s.totalTurns = entry.totalTurns;
|
|
141
|
-
s.sessionIds = {};
|
|
142
|
-
for (const toolId of ['claude', 'codex', 'codebuddy', 'opencode']) {
|
|
143
|
-
const sessionId = this.convSessionMap.get(this.getConvSessionKey(userId, entry.convId, toolId));
|
|
144
|
-
if (sessionId) {
|
|
145
|
-
if (!s.sessionIds)
|
|
146
|
-
s.sessionIds = {};
|
|
147
|
-
s.sessionIds[toolId] = sessionId;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
resumed = true;
|
|
151
|
-
log.info(`Resumed session for user ${userId}: convId=${entry.convId}, workDir=${realPath}, turns=${entry.totalTurns}`);
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
s.sessionIds = {};
|
|
155
|
-
s.activeConvId = randomBytes(4).toString('hex');
|
|
156
|
-
s.firstMessage = undefined;
|
|
157
|
-
}
|
|
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;
|
|
158
120
|
s.workDir = realPath;
|
|
159
121
|
}
|
|
160
122
|
else {
|
|
@@ -164,8 +126,8 @@ export class SessionManager {
|
|
|
164
126
|
});
|
|
165
127
|
}
|
|
166
128
|
this.flushSync();
|
|
167
|
-
log.info(`WorkDir changed for user ${userId}: ${realPath}
|
|
168
|
-
return { path: realPath, resumed };
|
|
129
|
+
log.info(`WorkDir changed for user ${userId}: ${realPath}`);
|
|
130
|
+
return { path: realPath, resumed: false };
|
|
169
131
|
}
|
|
170
132
|
newSession(userId) {
|
|
171
133
|
const s = this.sessions.get(userId);
|
|
@@ -173,31 +135,34 @@ export class SessionManager {
|
|
|
173
135
|
const oldSessionIds = { ...(s.sessionIds ?? {}) };
|
|
174
136
|
const oldConvId = s.activeConvId;
|
|
175
137
|
this.persistActiveConvSessions(userId, s);
|
|
176
|
-
// Archive old conversation to history (keep convSessionMap entries for resume)
|
|
177
|
-
if (oldConvId) {
|
|
178
|
-
if (!s.convHistory)
|
|
179
|
-
s.convHistory = [];
|
|
180
|
-
s.convHistory.push({
|
|
181
|
-
convId: oldConvId,
|
|
182
|
-
totalTurns: s.totalTurns ?? 0,
|
|
183
|
-
createdAt: Date.now(),
|
|
184
|
-
workDir: s.workDir,
|
|
185
|
-
firstMessage: s.firstMessage,
|
|
186
|
-
});
|
|
187
|
-
// Keep last 10 entries
|
|
188
|
-
if (s.convHistory.length > 10)
|
|
189
|
-
s.convHistory = s.convHistory.slice(-10);
|
|
190
|
-
}
|
|
191
138
|
s.sessionIds = {};
|
|
192
139
|
s.activeConvId = randomBytes(4).toString('hex');
|
|
193
140
|
s.totalTurns = 0;
|
|
194
|
-
s.firstMessage = undefined;
|
|
195
141
|
this.flushSync();
|
|
196
142
|
log.info(`New session for user ${userId}: oldConvId=${oldConvId}, oldSessionIds=${JSON.stringify(oldSessionIds)}, newConvId=${s.activeConvId}, sessionIds={}`);
|
|
197
143
|
return true;
|
|
198
144
|
}
|
|
199
145
|
return false;
|
|
200
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
|
+
}
|
|
201
166
|
clearActiveToolSession(userId, toolId) {
|
|
202
167
|
const s = this.sessions.get(userId);
|
|
203
168
|
if (!s)
|
|
@@ -212,59 +177,6 @@ export class SessionManager {
|
|
|
212
177
|
log.info(`Cleared active ${toolId} session for user ${userId}, convId=${activeConvId ?? 'none'}`);
|
|
213
178
|
return hadSession;
|
|
214
179
|
}
|
|
215
|
-
listConvHistory(userId) {
|
|
216
|
-
const s = this.sessions.get(userId);
|
|
217
|
-
if (!s)
|
|
218
|
-
return [];
|
|
219
|
-
return s.convHistory ?? [];
|
|
220
|
-
}
|
|
221
|
-
getActiveConvInfo(userId) {
|
|
222
|
-
const s = this.sessions.get(userId);
|
|
223
|
-
if (!s?.activeConvId)
|
|
224
|
-
return undefined;
|
|
225
|
-
return { convId: s.activeConvId, totalTurns: s.totalTurns ?? 0, firstMessage: s.firstMessage };
|
|
226
|
-
}
|
|
227
|
-
resumeConv(userId, convId) {
|
|
228
|
-
const s = this.sessions.get(userId);
|
|
229
|
-
if (!s)
|
|
230
|
-
return false;
|
|
231
|
-
// Find target in history
|
|
232
|
-
const idx = s.convHistory?.findIndex((e) => e.convId === convId) ?? -1;
|
|
233
|
-
if (idx === -1)
|
|
234
|
-
return false;
|
|
235
|
-
// Archive current active session
|
|
236
|
-
this.persistActiveConvSessions(userId, s);
|
|
237
|
-
if (s.activeConvId) {
|
|
238
|
-
if (!s.convHistory)
|
|
239
|
-
s.convHistory = [];
|
|
240
|
-
s.convHistory.push({
|
|
241
|
-
convId: s.activeConvId,
|
|
242
|
-
totalTurns: s.totalTurns ?? 0,
|
|
243
|
-
createdAt: Date.now(),
|
|
244
|
-
workDir: s.workDir,
|
|
245
|
-
firstMessage: s.firstMessage,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
// Remove target from history
|
|
249
|
-
const [entry] = s.convHistory.splice(idx, 1);
|
|
250
|
-
// Restore target as active
|
|
251
|
-
s.activeConvId = entry.convId;
|
|
252
|
-
s.totalTurns = entry.totalTurns;
|
|
253
|
-
s.firstMessage = entry.firstMessage;
|
|
254
|
-
s.sessionIds = {};
|
|
255
|
-
// Restore sessionIds from convSessionMap
|
|
256
|
-
for (const toolId of ['claude', 'codex', 'codebuddy']) {
|
|
257
|
-
const sessionId = this.convSessionMap.get(this.getConvSessionKey(userId, entry.convId, toolId));
|
|
258
|
-
if (sessionId) {
|
|
259
|
-
if (!s.sessionIds)
|
|
260
|
-
s.sessionIds = {};
|
|
261
|
-
s.sessionIds[toolId] = sessionId;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
this.flushSync();
|
|
265
|
-
log.info(`Resumed session for user ${userId}: convId=${entry.convId}, turns=${entry.totalTurns}`);
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
180
|
addTurns(userId, turns) {
|
|
269
181
|
const s = this.sessions.get(userId);
|
|
270
182
|
if (!s)
|
|
@@ -286,14 +198,6 @@ export class SessionManager {
|
|
|
286
198
|
this.save();
|
|
287
199
|
return t.totalTurns;
|
|
288
200
|
}
|
|
289
|
-
/** Save the first message preview for the current conversation (only on first turn). */
|
|
290
|
-
setConvPreview(userId, preview) {
|
|
291
|
-
const s = this.sessions.get(userId);
|
|
292
|
-
if (!s || (s.totalTurns ?? 0) > 0)
|
|
293
|
-
return;
|
|
294
|
-
s.firstMessage = preview;
|
|
295
|
-
this.save();
|
|
296
|
-
}
|
|
297
201
|
getModel(userId, threadId) {
|
|
298
202
|
const s = this.sessions.get(userId);
|
|
299
203
|
if (threadId) {
|
|
@@ -359,6 +263,9 @@ export class SessionManager {
|
|
|
359
263
|
log.warn(`Legacy shared sessionId found for user ${k}; clearing it to avoid cross-tool resume conflicts`);
|
|
360
264
|
}
|
|
361
265
|
delete v.sessionId;
|
|
266
|
+
// Clean up legacy fields
|
|
267
|
+
delete v.firstMessage;
|
|
268
|
+
delete v.convHistory;
|
|
362
269
|
if (v.threads) {
|
|
363
270
|
for (const thread of Object.values(v.threads)) {
|
|
364
271
|
if (!thread.sessionIds)
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -153,7 +153,6 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
153
153
|
const aiCommand = resolvePlatformAiCommand(config, ctx.platform);
|
|
154
154
|
const startRun = () => {
|
|
155
155
|
log.info(`[AITask] Starting: userId=${ctx.userId}, initialSessionId=${currentSessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
|
|
156
|
-
sessionManager.setConvPreview(ctx.userId, prompt.slice(0, 80));
|
|
157
156
|
emitStructuredEvent('AITask', 'ai.task.start', {
|
|
158
157
|
platform: ctx.platform,
|
|
159
158
|
taskKey: hashUserId(ctx.taskKey),
|