foliko 1.1.68 → 1.1.69
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/.claude/settings.local.json +19 -10
- package/.dockerignore +45 -45
- package/.env.example +56 -56
- package/CLAUDE.md +2 -2
- package/README.md +13 -13
- package/SPEC.md +3 -3
- package/cli/src/commands/chat.js +2 -20
- package/cli/src/commands/list.js +7 -6
- package/cli/src/commands/plugin.js +3 -2
- package/cli/src/daemon.js +2 -2
- package/cli/src/ui/chat-ui-old.js +15 -4
- package/cli/src/ui/chat-ui.js +152 -38
- package/cli/src/ui/footer-bar.js +13 -14
- package/cli/src/utils/config.js +29 -0
- package/cli/src/utils/plugin-config.js +1 -1
- package/docker-compose.yml +33 -33
- package/docs/features.md +120 -120
- package/docs/quick-reference.md +160 -160
- package/docs/user-manual.md +1391 -1391
- package/examples/ambient-example.js +2 -2
- package/examples/bootstrap.js +3 -3
- package/examples/test-chat.js +1 -1
- package/examples/test-reload.js +1 -1
- package/examples/test-telegram.js +1 -1
- package/examples/test-tg-bot.js +1 -1
- package/examples/test-tg-simple.js +2 -2
- package/examples/test-tg.js +1 -1
- package/examples/test-think.js +1 -1
- package/examples/test-weixin-feishu.js +3 -3
- package/package.json +3 -1
- package/plugins/ambient-agent/index.js +1 -1
- package/plugins/audit-plugin.js +75 -24
- package/plugins/default-plugins.js +21 -11
- package/plugins/email/index.js +1 -1
- package/plugins/extension-executor-plugin.js +85 -7
- package/plugins/feishu-plugin.js +118 -16
- package/plugins/install-plugin.js +4 -4
- package/plugins/memory-plugin.js +1 -1
- package/plugins/plugin-manager-plugin.js +3 -3
- package/plugins/python-executor-plugin.js +1 -1
- package/plugins/python-plugin-loader.js +1 -1
- package/plugins/qq-plugin.js +143 -16
- package/plugins/rules-plugin.js +1 -1
- package/plugins/scheduler-plugin.js +2 -2
- package/plugins/session-plugin.js +309 -393
- package/plugins/storage-plugin.js +235 -176
- package/plugins/subagent-plugin.js +4 -4
- package/plugins/telegram-plugin.js +116 -17
- package/plugins/think-plugin.js +1 -1
- package/plugins/web-plugin.js +2 -2
- package/plugins/weixin-plugin.js +102 -19
- package/skills/find-skills/AGENTS.md +2 -2
- package/skills/find-skills/SKILL.md +133 -133
- package/skills/foliko-dev/AGENTS.md +236 -236
- package/skills/foliko-dev/SKILL.md +19 -19
- package/skills/mcp-usage/SKILL.md +200 -200
- package/skills/plugin-guide/SKILL.md +4 -4
- package/skills/python-plugin-dev/SKILL.md +5 -5
- package/skills/skill-guide/SKILL.md +104 -6
- package/skills/subagent-guide/SKILL.md +237 -237
- package/skills/workflow-guide/SKILL.md +646 -646
- package/src/capabilities/skill-manager.js +101 -2
- package/src/capabilities/workflow-engine.js +1 -1
- package/src/core/agent-chat.js +17 -14
- package/src/core/branch-summary-auto.js +206 -0
- package/src/core/chat-session.js +38 -8
- package/src/core/command-registry.js +200 -0
- package/src/core/constants.js +11 -11
- package/src/core/context-compressor.js +700 -149
- package/src/core/context-manager.js +0 -1
- package/src/core/enhanced-context-compressor.js +210 -0
- package/src/core/framework.js +140 -83
- package/src/core/jsonl-storage.js +253 -0
- package/src/core/plugin-manager.js +15 -10
- package/src/core/provider-registry.js +159 -0
- package/src/core/provider.js +2 -0
- package/src/core/session-entry.js +225 -0
- package/src/core/session-manager.js +701 -0
- package/src/core/storage-manager.js +494 -0
- package/src/core/sub-agent-config.js +1 -1
- package/src/core/token-counter.js +177 -58
- package/src/core/ui-extension-context.js +174 -0
- package/src/executors/mcp-executor.js +15 -6
- package/src/utils/logger.js +152 -180
- package/src/utils/plugin-helpers.js +2 -2
- package/website_v2/docs/api.html +1 -1
- package/website_v2/docs/configuration.html +2 -2
- package/website_v2/docs/plugin-development.html +4 -4
- package/website_v2/docs/project-structure.html +2 -2
- package/website_v2/docs/skill-development.html +2 -2
- package/website_v2/index.html +1 -1
- package/website_v2/styles/animations.css +7 -7
- package/.agent/agents/backend-dev.md +0 -102
- package/.agent/agents/data-analyst.md +0 -117
- package/.agent/agents/devops.md +0 -115
- package/.agent/agents/frontend-dev.md +0 -94
- package/.agent/agents/network-requester.md +0 -44
- package/.agent/agents/poster-designer.md +0 -52
- package/.agent/agents/product-manager.md +0 -85
- package/.agent/agents/qa-engineer.md +0 -100
- package/.agent/agents/security-engineer.md +0 -99
- package/.agent/agents/team-lead.md +0 -137
- package/.agent/agents/ui-designer.md +0 -116
- package/.agent/data/default.json +0 -58
- package/.agent/data/email/processed-emails.json +0 -1
- package/.agent/data/plugins-state.json +0 -207
- package/.agent/data/scheduler/tasks.json +0 -1
- package/.agent/data/web/web-config.json +0 -5
- package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
- package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
- package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
- package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
- package/.agent/data/weixin.json +0 -6
- package/.agent/mcp_config.json +0 -14
- package/.agent/memory/user/mof6gk94-kneeuh.md +0 -9
- package/.agent/package.json +0 -8
- package/.agent/plugins/marknative/README.md +0 -134
- package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
- package/.agent/plugins/marknative/fonts.zip +0 -0
- package/.agent/plugins/marknative/index.js +0 -256
- package/.agent/plugins/marknative/package.json +0 -12
- package/.agent/plugins/test-plugin.py +0 -99
- package/.agent/plugins.json +0 -14
- package/.agent/python-scripts/test_sample.py +0 -24
- package/.agent/sessions/cli_default.json +0 -4121
- package/.agent/skills/agent-browser/SKILL.md +0 -311
- package/.agent/skills/agent-browser/TEST_PLAN.md +0 -200
- package/.agent/skills/sysinfo/SKILL.md +0 -38
- package/.agent/skills/sysinfo/system-info.sh +0 -130
- package/.agent/skills/workflow/SKILL.md +0 -324
- package/.agent/test-agent.js +0 -35
- package/.agent/weixin.json +0 -6
- package/.agent/workflows/email-digest.json +0 -50
- package/.agent/workflows/file-backup.json +0 -21
- package/.agent/workflows/get-ip-notify.json +0 -32
- package/.agent/workflows/news-aggregator.json +0 -93
- package/.agent/workflows/news-dashboard-v2.json +0 -94
- package/.agent/workflows/notification-batch.json +0 -32
- package/src/core/session-context.js +0 -346
- package/src/core/session-storage.js +0 -295
|
@@ -1,249 +1,285 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session
|
|
3
|
-
*
|
|
2
|
+
* Session Plugin - Session management with JSONL tree structure
|
|
3
|
+
* Rewritten to use pi-style SessionManager
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const { Plugin } = require('../src/core/plugin-base')
|
|
7
|
-
const { logger } = require('../src/utils/logger')
|
|
8
|
-
const log = logger.child('Session')
|
|
9
|
-
const { z } = require('zod')
|
|
10
|
-
const { EventEmitter } = require('events')
|
|
6
|
+
const { Plugin } = require('../src/core/plugin-base');
|
|
7
|
+
const { logger } = require('../src/utils/logger');
|
|
8
|
+
const log = logger.child('Session');
|
|
9
|
+
const { z } = require('zod');
|
|
10
|
+
const { EventEmitter } = require('events');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { SessionManager } = require('../src/core/session-manager');
|
|
13
|
+
const { buildSessionContext, EntryTypes } = require('../src/core/session-entry');
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
|
-
*
|
|
16
|
+
* Get default session directory for a cwd
|
|
14
17
|
*/
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
20
|
-
const r = Math.random() * 16 | 0
|
|
21
|
-
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
|
22
|
-
return v.toString(16)
|
|
23
|
-
})
|
|
18
|
+
function getDefaultSessionDir(cwd) {
|
|
19
|
+
const agentDir = path.join(process.cwd(), '.foliko');
|
|
20
|
+
const sessionDir = path.join(agentDir, 'sessions');
|
|
21
|
+
return sessionDir;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
class SessionPlugin extends Plugin {
|
|
27
25
|
constructor(config = {}) {
|
|
28
|
-
super()
|
|
29
|
-
this.name = 'session'
|
|
30
|
-
this.version = '
|
|
31
|
-
this.description = '
|
|
32
|
-
this.priority = 5
|
|
26
|
+
super();
|
|
27
|
+
this.name = 'session';
|
|
28
|
+
this.version = '2.0.0';
|
|
29
|
+
this.description = '会话管理插件,支持多会话隔离、历史记录、树形分支结构';
|
|
30
|
+
this.priority = 5;
|
|
33
31
|
|
|
34
|
-
this.system = true
|
|
32
|
+
this.system = true;
|
|
35
33
|
|
|
36
34
|
this.config = {
|
|
37
|
-
sessionTTL: config.sessionTTL || 30 * 60 * 1000,
|
|
35
|
+
sessionTTL: config.sessionTTL || 30 * 60 * 1000,
|
|
38
36
|
maxSessions: config.maxSessions || 100,
|
|
39
|
-
maxHistoryLength: config.maxHistoryLength || 150,
|
|
40
|
-
autoCleanup: config.autoCleanup
|
|
41
|
-
cleanupInterval: config.cleanupInterval || 5 * 60 * 1000,
|
|
42
|
-
persistToStorage: config.persistToStorage !== false,
|
|
43
|
-
// Session 钩子配置
|
|
37
|
+
maxHistoryLength: config.maxHistoryLength || 150,
|
|
38
|
+
autoCleanup: config.autoCleanup !== undefined ? config.autoCleanup : true,
|
|
39
|
+
cleanupInterval: config.cleanupInterval || 5 * 60 * 1000,
|
|
40
|
+
persistToStorage: config.persistToStorage !== false,
|
|
44
41
|
onSessionStart: config.onSessionStart || null,
|
|
45
42
|
onSessionEnd: config.onSessionEnd || null,
|
|
46
43
|
onSessionCreated: config.onSessionCreated || null
|
|
47
|
-
}
|
|
48
|
-
// console.log('SessionPlugin config:', this.config)
|
|
49
|
-
this._framework = null
|
|
50
|
-
this._sessions = new Map()
|
|
51
|
-
this._cleanupTimer = null
|
|
52
|
-
this._events = new EventEmitter()
|
|
53
|
-
this._storageKeyPrefix = 'session:'
|
|
54
|
-
}
|
|
44
|
+
};
|
|
55
45
|
|
|
56
|
-
|
|
57
|
-
this.
|
|
58
|
-
|
|
46
|
+
this._framework = null;
|
|
47
|
+
this._sessionManagers = new Map(); // sessionId -> SessionManager
|
|
48
|
+
this._cleanupTimer = null;
|
|
49
|
+
this._events = new EventEmitter();
|
|
59
50
|
}
|
|
60
51
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*/
|
|
65
|
-
_getStorage() {
|
|
66
|
-
if (!this._framework) return null;
|
|
67
|
-
return this._framework.pluginManager.get('storage');
|
|
52
|
+
install(framework) {
|
|
53
|
+
this._framework = framework;
|
|
54
|
+
return this;
|
|
68
55
|
}
|
|
69
56
|
|
|
70
57
|
/**
|
|
71
|
-
*
|
|
72
|
-
* @private
|
|
58
|
+
* Get or create a SessionManager for a session
|
|
73
59
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const store = storage.getStore();
|
|
80
|
-
const data = store.get(this._storageKeyPrefix + sessionId);
|
|
81
|
-
if (data && data.messages) {
|
|
82
|
-
return data;
|
|
83
|
-
}
|
|
84
|
-
} catch (err) {
|
|
85
|
-
// 忽略加载错误
|
|
60
|
+
_getSessionManager(sessionId, cwd = process.cwd()) {
|
|
61
|
+
if (this._sessionManagers.has(sessionId)) {
|
|
62
|
+
return this._sessionManagers.get(sessionId);
|
|
86
63
|
}
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* 保存会话数据到 storage
|
|
92
|
-
* @private
|
|
93
|
-
*/
|
|
94
|
-
_saveToStorage(session) {
|
|
95
|
-
const storage = this._getStorage();
|
|
96
|
-
if (!storage || !this.config.persistToStorage) return;
|
|
97
64
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
createdAt: session.createdAt,
|
|
104
|
-
lastActive: session.lastActive
|
|
105
|
-
});
|
|
106
|
-
} catch (err) {
|
|
107
|
-
// 忽略保存错误
|
|
108
|
-
}
|
|
65
|
+
const sessionDir = getDefaultSessionDir(cwd);
|
|
66
|
+
const sessionFile = path.join(sessionDir, `${sessionId}.jsonl`);
|
|
67
|
+
const manager = new SessionManager(cwd, sessionDir, sessionFile, true);
|
|
68
|
+
this._sessionManagers.set(sessionId, manager);
|
|
69
|
+
return manager;
|
|
109
70
|
}
|
|
110
71
|
|
|
111
72
|
/**
|
|
112
|
-
*
|
|
113
|
-
* @private
|
|
73
|
+
* List all sessions for current cwd
|
|
114
74
|
*/
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
75
|
+
async listSessions(cwd = process.cwd()) {
|
|
76
|
+
const sessionDir = getDefaultSessionDir(cwd);
|
|
77
|
+
const sessions = [];
|
|
118
78
|
|
|
119
79
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* 从 storage 加载所有会话
|
|
128
|
-
* @private
|
|
129
|
-
*/
|
|
130
|
-
_loadAllFromStorage() {
|
|
131
|
-
const storage = this._getStorage();
|
|
132
|
-
if (!storage) return;
|
|
80
|
+
if (!require('fs').existsSync(sessionDir)) {
|
|
81
|
+
return sessions;
|
|
82
|
+
}
|
|
133
83
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
for (const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
lastActive: sessionData.lastActive ? new Date(sessionData.lastActive) : new Date()
|
|
150
|
-
});
|
|
151
|
-
}
|
|
84
|
+
const files = require('fs').readdirSync(sessionDir)
|
|
85
|
+
.filter(f => f.endsWith('.jsonl'));
|
|
86
|
+
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
const filePath = path.join(sessionDir, file);
|
|
89
|
+
try {
|
|
90
|
+
const manager = SessionManager.open(filePath, sessionDir, cwd);
|
|
91
|
+
sessions.push({
|
|
92
|
+
sessionId: manager.getSessionId(),
|
|
93
|
+
sessionFile: manager.getSessionFile(),
|
|
94
|
+
messageCount: manager.getEntries().length,
|
|
95
|
+
leafId: manager.getLeafId()
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
// Skip invalid session files
|
|
152
99
|
}
|
|
153
100
|
}
|
|
154
|
-
log.info(`Loaded ${this._sessions.size} sessions from storage`);
|
|
155
101
|
} catch (err) {
|
|
156
|
-
log.warn('Failed to
|
|
102
|
+
log.warn('Failed to list sessions:', err.message);
|
|
157
103
|
}
|
|
104
|
+
|
|
105
|
+
return sessions;
|
|
158
106
|
}
|
|
159
107
|
|
|
160
108
|
start(framework) {
|
|
161
|
-
//
|
|
162
|
-
this._loadAllFromStorage();
|
|
163
|
-
// 注册会话管理工具
|
|
109
|
+
// Register session management tools
|
|
164
110
|
framework.registerTool({
|
|
165
111
|
name: 'session_create',
|
|
166
112
|
description: '创建新会话',
|
|
167
113
|
inputSchema: z.object({
|
|
168
114
|
sessionId: z.string().optional().describe('可选:指定会话 ID'),
|
|
169
|
-
|
|
115
|
+
cwd: z.string().optional().describe('工作目录')
|
|
170
116
|
}),
|
|
171
|
-
execute: async (args) => {
|
|
172
|
-
const
|
|
173
|
-
const
|
|
117
|
+
execute: async (args, fw) => {
|
|
118
|
+
const cwd = args.cwd || process.cwd();
|
|
119
|
+
const sessionDir = getDefaultSessionDir(cwd);
|
|
120
|
+
const manager = new SessionManager(cwd, sessionDir, undefined, true);
|
|
121
|
+
const sessionId = manager.getSessionId();
|
|
122
|
+
this._sessionManagers.set(sessionId, manager);
|
|
123
|
+
|
|
174
124
|
return {
|
|
175
125
|
success: true,
|
|
176
|
-
data: {
|
|
177
|
-
|
|
126
|
+
data: {
|
|
127
|
+
sessionId,
|
|
128
|
+
sessionFile: manager.getSessionFile(),
|
|
129
|
+
createdAt: new Date().toISOString()
|
|
130
|
+
}
|
|
131
|
+
};
|
|
178
132
|
}
|
|
179
|
-
})
|
|
133
|
+
});
|
|
180
134
|
|
|
181
135
|
framework.registerTool({
|
|
182
136
|
name: 'session_get',
|
|
183
137
|
description: '获取会话信息',
|
|
184
138
|
inputSchema: z.object({
|
|
185
|
-
sessionId: z.string().describe('会话 ID')
|
|
139
|
+
sessionId: z.string().describe('会话 ID'),
|
|
140
|
+
cwd: z.string().optional().describe('工作目录')
|
|
186
141
|
}),
|
|
187
142
|
execute: async (args) => {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
143
|
+
const cwd = args.cwd || process.cwd();
|
|
144
|
+
const manager = this._getSessionManager(args.sessionId, cwd);
|
|
145
|
+
const entries = manager.getEntries();
|
|
146
|
+
|
|
192
147
|
return {
|
|
193
148
|
success: true,
|
|
194
149
|
data: {
|
|
195
|
-
sessionId:
|
|
196
|
-
messageCount:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
lastActive: session.lastActive
|
|
150
|
+
sessionId: manager.getSessionId(),
|
|
151
|
+
messageCount: entries.length,
|
|
152
|
+
leafId: manager.getLeafId(),
|
|
153
|
+
sessionFile: manager.getSessionFile()
|
|
200
154
|
}
|
|
201
|
-
}
|
|
155
|
+
};
|
|
202
156
|
}
|
|
203
|
-
})
|
|
157
|
+
});
|
|
204
158
|
|
|
205
159
|
framework.registerTool({
|
|
206
160
|
name: 'session_list',
|
|
207
161
|
description: '列出所有会话',
|
|
208
|
-
inputSchema: z.object({
|
|
209
|
-
|
|
210
|
-
|
|
162
|
+
inputSchema: z.object({
|
|
163
|
+
cwd: z.string().optional().describe('工作目录')
|
|
164
|
+
}),
|
|
165
|
+
execute: async (args) => {
|
|
166
|
+
const cwd = args.cwd || process.cwd();
|
|
167
|
+
const sessions = await this.listSessions(cwd);
|
|
211
168
|
return {
|
|
212
169
|
success: true,
|
|
213
170
|
data: sessions,
|
|
214
171
|
metadata: { total: sessions.length }
|
|
215
|
-
}
|
|
172
|
+
};
|
|
216
173
|
}
|
|
217
|
-
})
|
|
174
|
+
});
|
|
218
175
|
|
|
219
176
|
framework.registerTool({
|
|
220
177
|
name: 'session_delete',
|
|
221
178
|
description: '删除会话',
|
|
222
179
|
inputSchema: z.object({
|
|
223
|
-
sessionId: z.string().describe('会话 ID')
|
|
180
|
+
sessionId: z.string().describe('会话 ID'),
|
|
181
|
+
cwd: z.string().optional().describe('工作目录')
|
|
224
182
|
}),
|
|
225
183
|
execute: async (args) => {
|
|
226
|
-
const
|
|
227
|
-
|
|
184
|
+
const cwd = args.cwd || process.cwd();
|
|
185
|
+
const manager = this._getSessionManager(args.sessionId, cwd);
|
|
186
|
+
|
|
187
|
+
if (manager.getSessionFile() && require('fs').existsSync(manager.getSessionFile())) {
|
|
188
|
+
require('fs').unlinkSync(manager.getSessionFile());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this._sessionManagers.delete(args.sessionId);
|
|
192
|
+
|
|
193
|
+
return { success: true, data: `Session ${args.sessionId} deleted` };
|
|
228
194
|
}
|
|
229
|
-
})
|
|
195
|
+
});
|
|
230
196
|
|
|
231
197
|
framework.registerTool({
|
|
232
198
|
name: 'session_history',
|
|
233
199
|
description: '获取会话历史消息',
|
|
234
200
|
inputSchema: z.object({
|
|
235
201
|
sessionId: z.string().describe('会话 ID'),
|
|
202
|
+
cwd: z.string().optional().describe('工作目录'),
|
|
236
203
|
limit: z.number().optional().describe('返回消息数量限制')
|
|
237
204
|
}),
|
|
238
205
|
execute: async (args) => {
|
|
239
|
-
const
|
|
206
|
+
const cwd = args.cwd || process.cwd();
|
|
207
|
+
const manager = this._getSessionManager(args.sessionId, cwd);
|
|
208
|
+
const context = manager.buildSessionContext();
|
|
209
|
+
let messages = context.messages;
|
|
210
|
+
|
|
211
|
+
if (args.limit) {
|
|
212
|
+
messages = messages.slice(-args.limit);
|
|
213
|
+
}
|
|
214
|
+
|
|
240
215
|
return {
|
|
241
216
|
success: true,
|
|
242
|
-
data:
|
|
243
|
-
metadata: {
|
|
217
|
+
data: messages,
|
|
218
|
+
metadata: {
|
|
219
|
+
sessionId: args.sessionId,
|
|
220
|
+
count: messages.length,
|
|
221
|
+
thinkingLevel: context.thinkingLevel
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
framework.registerTool({
|
|
228
|
+
name: 'session_branch',
|
|
229
|
+
description: '创建新分支',
|
|
230
|
+
inputSchema: z.object({
|
|
231
|
+
sessionId: z.string().describe('会话 ID'),
|
|
232
|
+
entryId: z.string().describe('分支点 entry ID'),
|
|
233
|
+
cwd: z.string().optional().describe('工作目录')
|
|
234
|
+
}),
|
|
235
|
+
execute: async (args) => {
|
|
236
|
+
const cwd = args.cwd || process.cwd();
|
|
237
|
+
const manager = this._getSessionManager(args.sessionId, cwd);
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
manager.branch(args.entryId);
|
|
241
|
+
return {
|
|
242
|
+
success: true,
|
|
243
|
+
data: {
|
|
244
|
+
sessionId: args.sessionId,
|
|
245
|
+
newLeafId: args.entryId,
|
|
246
|
+
message: `Branch created from entry ${args.entryId}`
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return { success: false, error: err.message };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
framework.registerTool({
|
|
256
|
+
name: 'session_branch_with_summary',
|
|
257
|
+
description: '创建带摘要的分支',
|
|
258
|
+
inputSchema: z.object({
|
|
259
|
+
sessionId: z.string().describe('会话 ID'),
|
|
260
|
+
entryId: z.string().optional().describe('分支点 entry ID'),
|
|
261
|
+
summary: z.string().describe('分支摘要'),
|
|
262
|
+
cwd: z.string().optional().describe('工作目录')
|
|
263
|
+
}),
|
|
264
|
+
execute: async (args) => {
|
|
265
|
+
const cwd = args.cwd || process.cwd();
|
|
266
|
+
const manager = this._getSessionManager(args.sessionId, cwd);
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const branchId = manager.branchWithSummary(args.entryId || null, args.summary);
|
|
270
|
+
return {
|
|
271
|
+
success: true,
|
|
272
|
+
data: {
|
|
273
|
+
sessionId: args.sessionId,
|
|
274
|
+
branchId,
|
|
275
|
+
message: `Branch created with summary`
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
} catch (err) {
|
|
279
|
+
return { success: false, error: err.message };
|
|
244
280
|
}
|
|
245
281
|
}
|
|
246
|
-
})
|
|
282
|
+
});
|
|
247
283
|
|
|
248
284
|
framework.registerTool({
|
|
249
285
|
name: 'session_stats',
|
|
@@ -252,321 +288,201 @@ class SessionPlugin extends Plugin {
|
|
|
252
288
|
execute: async () => {
|
|
253
289
|
return {
|
|
254
290
|
success: true,
|
|
255
|
-
data:
|
|
256
|
-
|
|
291
|
+
data: {
|
|
292
|
+
activeSessions: this._sessionManagers.size,
|
|
293
|
+
maxSessions: this.config.maxSessions
|
|
294
|
+
}
|
|
295
|
+
};
|
|
257
296
|
}
|
|
258
|
-
})
|
|
297
|
+
});
|
|
259
298
|
|
|
260
|
-
//
|
|
299
|
+
// Start auto cleanup if enabled
|
|
261
300
|
if (this.config.autoCleanup) {
|
|
262
|
-
this._startAutoCleanup()
|
|
301
|
+
this._startAutoCleanup();
|
|
263
302
|
}
|
|
264
303
|
|
|
265
|
-
return this
|
|
304
|
+
return this;
|
|
266
305
|
}
|
|
267
306
|
|
|
268
307
|
/**
|
|
269
|
-
*
|
|
308
|
+
* Add message to session
|
|
270
309
|
*/
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
if (this._sessions.has(id)) {
|
|
275
|
-
return this._sessions.get(id)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// 如果会话数超过限制,清理最老的
|
|
279
|
-
// if (this._sessions.size >= this.config.maxSessions) {
|
|
280
|
-
// this._cleanupLRU()
|
|
281
|
-
// }
|
|
282
|
-
|
|
283
|
-
const session = {
|
|
284
|
-
id,
|
|
285
|
-
messages: options.initialMessages || [],
|
|
286
|
-
variables: options.variables || {},
|
|
287
|
-
metadata: options.metadata || {},
|
|
288
|
-
createdAt: new Date(),
|
|
289
|
-
lastActive: new Date()
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
this._sessions.set(id, session)
|
|
293
|
-
this._saveToStorage(session)
|
|
294
|
-
|
|
295
|
-
// 触发 session:created 事件
|
|
296
|
-
this._events.emit('session:created', session)
|
|
310
|
+
addMessage(sessionId, message, cwd = process.cwd()) {
|
|
311
|
+
const manager = this._getSessionManager(sessionId, cwd);
|
|
312
|
+
const entryId = manager.appendMessage(message);
|
|
297
313
|
|
|
298
|
-
//
|
|
299
|
-
if (
|
|
300
|
-
this.
|
|
314
|
+
// Emit session event
|
|
315
|
+
if (manager.getEntries().length === 1) {
|
|
316
|
+
this._events.emit('session:created', { sessionId, manager });
|
|
301
317
|
}
|
|
302
318
|
|
|
303
|
-
return
|
|
319
|
+
return entryId;
|
|
304
320
|
}
|
|
305
321
|
|
|
306
322
|
/**
|
|
307
|
-
*
|
|
323
|
+
* Get session context for LLM
|
|
308
324
|
*/
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
session.lastActive = new Date()
|
|
313
|
-
return session
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// 尝试从 storage 加载
|
|
317
|
-
const storageData = this._loadFromStorage(sessionId)
|
|
318
|
-
if (storageData) {
|
|
319
|
-
session = {
|
|
320
|
-
id: sessionId,
|
|
321
|
-
messages: storageData.messages || [],
|
|
322
|
-
variables: storageData.variables || {},
|
|
323
|
-
metadata: storageData.metadata || {},
|
|
324
|
-
createdAt: storageData.createdAt ? new Date(storageData.createdAt) : new Date(),
|
|
325
|
-
lastActive: new Date()
|
|
326
|
-
}
|
|
327
|
-
this._sessions.set(sessionId, session)
|
|
328
|
-
return session
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return null
|
|
325
|
+
getSessionContext(sessionId, cwd = process.cwd()) {
|
|
326
|
+
const manager = this._getSessionManager(sessionId, cwd);
|
|
327
|
+
return manager.buildSessionContext();
|
|
332
328
|
}
|
|
333
329
|
|
|
334
330
|
/**
|
|
335
|
-
*
|
|
331
|
+
* Get or create session (alias for getSessionContext)
|
|
336
332
|
*/
|
|
337
|
-
getOrCreateSession(sessionId, options = {}) {
|
|
338
|
-
|
|
339
|
-
if (!session) {
|
|
340
|
-
session = this.createSession(sessionId, options)
|
|
341
|
-
}
|
|
342
|
-
return session
|
|
333
|
+
getOrCreateSession(sessionId, options = {}, cwd = process.cwd()) {
|
|
334
|
+
return this.getSessionContext(sessionId, cwd);
|
|
343
335
|
}
|
|
344
336
|
|
|
345
337
|
/**
|
|
346
|
-
*
|
|
338
|
+
* Get specific entry
|
|
347
339
|
*/
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
// 基础消息验证
|
|
352
|
-
if (!message || !message.role || !message.content) {
|
|
353
|
-
log.warn('Invalid message format, skipping:', { sessionId, message });
|
|
354
|
-
return session;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// 触发 session:started 事件(首次添加消息时)
|
|
358
|
-
if (session.messages.length === 0) {
|
|
359
|
-
this._events.emit('session:started', session)
|
|
360
|
-
if (this.config.onSessionStart) {
|
|
361
|
-
this._callHook(this.config.onSessionStart, session)
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
session.messages.push(message)
|
|
366
|
-
session.lastActive = new Date()
|
|
367
|
-
|
|
368
|
-
// 限制历史长度
|
|
369
|
-
if (session.messages.length > this.config.maxHistoryLength) {
|
|
370
|
-
session.messages = session.messages.slice(-this.config.maxHistoryLength)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// 持久化到 storage
|
|
374
|
-
this._saveToStorage(session)
|
|
375
|
-
}
|
|
376
|
-
return session
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* 整体替换会话消息(用于压缩后同步)
|
|
381
|
-
* @param {string} sessionId - 会话 ID
|
|
382
|
-
* @param {Array} messages - 新的消息数组
|
|
383
|
-
*/
|
|
384
|
-
replaceMessages(sessionId, messages) {
|
|
385
|
-
const session = this.getSession(sessionId)
|
|
386
|
-
if (session) {
|
|
387
|
-
session.messages = messages || []
|
|
388
|
-
session.lastActive = new Date()
|
|
389
|
-
// 持久化到 storage
|
|
390
|
-
this._saveToStorage(session)
|
|
391
|
-
}
|
|
392
|
-
return session
|
|
340
|
+
getEntry(sessionId, entryId, cwd = process.cwd()) {
|
|
341
|
+
const manager = this._getSessionManager(sessionId, cwd);
|
|
342
|
+
return manager.getEntry(entryId);
|
|
393
343
|
}
|
|
394
344
|
|
|
395
345
|
/**
|
|
396
|
-
*
|
|
346
|
+
* Get branch (path from root to leaf)
|
|
397
347
|
*/
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const messages = session.messages
|
|
403
|
-
if (limit) {
|
|
404
|
-
return messages.slice(-limit)
|
|
405
|
-
}
|
|
406
|
-
return messages
|
|
348
|
+
getBranch(sessionId, fromId, cwd = process.cwd()) {
|
|
349
|
+
const manager = this._getSessionManager(sessionId, cwd);
|
|
350
|
+
return manager.getBranch(fromId);
|
|
407
351
|
}
|
|
408
352
|
|
|
409
353
|
/**
|
|
410
|
-
*
|
|
354
|
+
* Append compaction entry
|
|
411
355
|
*/
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
this._deleteFromStorage(sessionId)
|
|
416
|
-
this._events.emit('session:deleted', sessionId)
|
|
417
|
-
}
|
|
418
|
-
return deleted
|
|
356
|
+
appendCompaction(sessionId, summary, firstKeptEntryId, tokensBefore, details, cwd = process.cwd()) {
|
|
357
|
+
const manager = this._getSessionManager(sessionId, cwd);
|
|
358
|
+
return manager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, false);
|
|
419
359
|
}
|
|
420
360
|
|
|
421
361
|
/**
|
|
422
|
-
*
|
|
362
|
+
* Append thinking level change
|
|
423
363
|
*/
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
messageCount: s.messages.length,
|
|
428
|
-
createdAt: s.createdAt,
|
|
429
|
-
lastActive: s.lastActive,
|
|
430
|
-
metadata: s.metadata
|
|
431
|
-
}))
|
|
364
|
+
appendThinkingLevelChange(sessionId, thinkingLevel, cwd = process.cwd()) {
|
|
365
|
+
const manager = this._getSessionManager(sessionId, cwd);
|
|
366
|
+
return manager.appendThinkingLevelChange(thinkingLevel);
|
|
432
367
|
}
|
|
433
368
|
|
|
434
369
|
/**
|
|
435
|
-
*
|
|
370
|
+
* Append model change
|
|
436
371
|
*/
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
for (const [sessionId, session] of this._sessions) {
|
|
442
|
-
if (now - session.lastActive.getTime() > this.config.sessionTTL) {
|
|
443
|
-
this._sessions.delete(sessionId)
|
|
444
|
-
cleanedCount++
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return cleanedCount
|
|
372
|
+
appendModelChange(sessionId, provider, modelId, cwd = process.cwd()) {
|
|
373
|
+
const manager = this._getSessionManager(sessionId, cwd);
|
|
374
|
+
return manager.appendModelChange(provider, modelId);
|
|
449
375
|
}
|
|
450
376
|
|
|
451
377
|
/**
|
|
452
|
-
*
|
|
378
|
+
* Set session label
|
|
453
379
|
*/
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
// 按最后活跃时间排序
|
|
459
|
-
sessions.sort((a, b) => a.lastActive.getTime() - b.lastActive.getTime())
|
|
460
|
-
|
|
461
|
-
// 删除最老的会话
|
|
462
|
-
const toDelete = sessions.slice(0, Math.ceil(sessions.length * 0.2))
|
|
463
|
-
for (const session of toDelete) {
|
|
464
|
-
this._sessions.delete(session.id)
|
|
465
|
-
}
|
|
380
|
+
setLabel(sessionId, entryId, label, cwd = process.cwd()) {
|
|
381
|
+
const manager = this._getSessionManager(sessionId, cwd);
|
|
382
|
+
return manager.appendLabelChange(entryId, label);
|
|
466
383
|
}
|
|
467
384
|
|
|
468
385
|
/**
|
|
469
|
-
*
|
|
386
|
+
* Get session info (compatible with external callers)
|
|
470
387
|
*/
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
const
|
|
474
|
-
(sum, s) => sum + s.messages.length, 0
|
|
475
|
-
)
|
|
476
|
-
|
|
388
|
+
getSession(sessionId, cwd = process.cwd()) {
|
|
389
|
+
const manager = this._getSessionManager(sessionId, cwd);
|
|
390
|
+
const entries = manager.getEntries();
|
|
477
391
|
return {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
392
|
+
id: sessionId,
|
|
393
|
+
messages: entries.filter(e => e.type === 'message'),
|
|
394
|
+
lastActive: manager.getHeader()?.timestamp ? new Date(manager.getHeader().timestamp) : null,
|
|
395
|
+
};
|
|
483
396
|
}
|
|
484
397
|
|
|
485
398
|
/**
|
|
486
|
-
*
|
|
399
|
+
* Start auto cleanup timer
|
|
487
400
|
*/
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
*/
|
|
497
|
-
on(event, listener) {
|
|
498
|
-
this._events.on(event, listener)
|
|
401
|
+
_startAutoCleanup() {
|
|
402
|
+
if (this._cleanupTimer) {
|
|
403
|
+
clearInterval(this._cleanupTimer);
|
|
404
|
+
}
|
|
405
|
+
this._cleanupTimer = setInterval(async () => {
|
|
406
|
+
await this._doCleanup();
|
|
407
|
+
}, this.config.cleanupInterval);
|
|
408
|
+
log.info(`Auto cleanup started (interval: ${this.config.cleanupInterval}ms)`);
|
|
499
409
|
}
|
|
500
410
|
|
|
501
411
|
/**
|
|
502
|
-
*
|
|
412
|
+
* Perform cleanup of old sessions and message history
|
|
503
413
|
*/
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
414
|
+
async _doCleanup() {
|
|
415
|
+
try {
|
|
416
|
+
const fs = require('fs');
|
|
417
|
+
const sessionDir = getDefaultSessionDir(process.cwd());
|
|
418
|
+
|
|
419
|
+
if (!fs.existsSync(sessionDir)) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
507
422
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
423
|
+
const files = fs.readdirSync(sessionDir).filter(f => f.endsWith('.jsonl'));
|
|
424
|
+
let cleanedCount = 0;
|
|
425
|
+
|
|
426
|
+
for (const file of files) {
|
|
427
|
+
const filePath = path.join(sessionDir, file);
|
|
428
|
+
try {
|
|
429
|
+
const manager = SessionManager.open(filePath, sessionDir, process.cwd());
|
|
430
|
+
const entries = manager.getEntries();
|
|
431
|
+
|
|
432
|
+
// Clean up if exceeds maxHistoryLength
|
|
433
|
+
if (entries.length > this.config.maxHistoryLength) {
|
|
434
|
+
const toRemove = entries.length - this.config.maxHistoryLength;
|
|
435
|
+
// Keep session header, remove oldest entries
|
|
436
|
+
const entriesToKeep = manager.fileEntries.filter(e => e.type === 'session');
|
|
437
|
+
const messagesToKeep = manager.fileEntries
|
|
438
|
+
.filter(e => e.type !== 'session')
|
|
439
|
+
.slice(-this.config.maxHistoryLength);
|
|
440
|
+
|
|
441
|
+
manager.fileEntries = [...entriesToKeep, ...messagesToKeep];
|
|
442
|
+
manager._rewriteFile();
|
|
443
|
+
cleanedCount++;
|
|
444
|
+
log.debug(`Cleaned ${file}: removed ${toRemove} entries, kept ${this.config.maxHistoryLength}`);
|
|
445
|
+
}
|
|
446
|
+
} catch (err) {
|
|
447
|
+
log.warn(`Failed to cleanup session ${file}: ${err.message}`);
|
|
520
448
|
}
|
|
521
|
-
} catch (err) {
|
|
522
|
-
log.warn('Session hook error:', err.message)
|
|
523
449
|
}
|
|
450
|
+
|
|
451
|
+
if (cleanedCount > 0) {
|
|
452
|
+
log.info(`Auto cleanup completed: cleaned ${cleanedCount} sessions`);
|
|
453
|
+
}
|
|
454
|
+
} catch (err) {
|
|
455
|
+
log.error(`Auto cleanup error: ${err.message}`);
|
|
524
456
|
}
|
|
525
457
|
}
|
|
526
458
|
|
|
527
459
|
/**
|
|
528
|
-
*
|
|
529
|
-
* 当会话完成处理时调用
|
|
530
|
-
* @param {string} sessionId - 会话 ID
|
|
460
|
+
* On session event listener
|
|
531
461
|
*/
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (session) {
|
|
535
|
-
this._events.emit('session:ended', session)
|
|
536
|
-
if (this.config.onSessionEnd) {
|
|
537
|
-
this._callHook(this.config.onSessionEnd, session)
|
|
538
|
-
}
|
|
539
|
-
}
|
|
462
|
+
on(event, listener) {
|
|
463
|
+
this._events.on(event, listener);
|
|
540
464
|
}
|
|
541
465
|
|
|
542
466
|
/**
|
|
543
|
-
*
|
|
467
|
+
* Off session event listener
|
|
544
468
|
*/
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
clearInterval(this._cleanupTimer)
|
|
548
|
-
}
|
|
549
|
-
this._cleanupTimer = setInterval(() => {
|
|
550
|
-
const cleaned = this.cleanup()
|
|
551
|
-
if (cleaned > 0) {
|
|
552
|
-
//log.info(` Cleaned up ${cleaned} expired sessions`)
|
|
553
|
-
}
|
|
554
|
-
}, this.config.cleanupInterval)
|
|
469
|
+
off(event, listener) {
|
|
470
|
+
this._events.off(event, listener);
|
|
555
471
|
}
|
|
556
472
|
|
|
557
473
|
reload(framework) {
|
|
558
|
-
this._framework = framework
|
|
474
|
+
this._framework = framework;
|
|
559
475
|
}
|
|
560
476
|
|
|
561
477
|
uninstall(framework) {
|
|
562
478
|
if (this._cleanupTimer) {
|
|
563
|
-
clearInterval(this._cleanupTimer)
|
|
564
|
-
this._cleanupTimer = null
|
|
479
|
+
clearInterval(this._cleanupTimer);
|
|
480
|
+
this._cleanupTimer = null;
|
|
565
481
|
}
|
|
566
|
-
this._events.removeAllListeners()
|
|
567
|
-
this.
|
|
568
|
-
this._framework = null
|
|
482
|
+
this._events.removeAllListeners();
|
|
483
|
+
this._sessionManagers.clear();
|
|
484
|
+
this._framework = null;
|
|
569
485
|
}
|
|
570
486
|
}
|
|
571
487
|
|
|
572
|
-
module.exports = SessionPlugin
|
|
488
|
+
module.exports = { SessionPlugin };
|