@geekbeer/minion 3.11.5 → 3.13.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.
- package/core/db.js +71 -40
- package/core/stores/chat-store.js +57 -21
- package/linux/routes/chat.js +55 -34
- package/package.json +1 -1
- package/win/routes/chat.js +52 -30
package/core/db.js
CHANGED
|
@@ -195,6 +195,7 @@ function initSchema(db) {
|
|
|
195
195
|
-- ==================== chat_sessions ====================
|
|
196
196
|
CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
197
197
|
session_id TEXT PRIMARY KEY,
|
|
198
|
+
workspace_id TEXT,
|
|
198
199
|
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
199
200
|
created_at INTEGER NOT NULL,
|
|
200
201
|
updated_at INTEGER NOT NULL
|
|
@@ -390,57 +391,87 @@ function migrateSchema(db) {
|
|
|
390
391
|
}
|
|
391
392
|
|
|
392
393
|
if (currentVersion < 2) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
INSERT
|
|
406
|
-
|
|
407
|
-
|
|
394
|
+
try {
|
|
395
|
+
console.log('[DB] Migration 2: Adding emails FTS5...')
|
|
396
|
+
|
|
397
|
+
db.exec(`
|
|
398
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS emails_fts USING fts5(
|
|
399
|
+
subject,
|
|
400
|
+
body_text,
|
|
401
|
+
content=emails,
|
|
402
|
+
content_rowid=rowid,
|
|
403
|
+
tokenize='trigram'
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
CREATE TRIGGER IF NOT EXISTS emails_ai AFTER INSERT ON emails BEGIN
|
|
407
|
+
INSERT INTO emails_fts(rowid, subject, body_text)
|
|
408
|
+
VALUES (new.rowid, new.subject, new.body_text);
|
|
409
|
+
END;
|
|
410
|
+
|
|
411
|
+
CREATE TRIGGER IF NOT EXISTS emails_ad AFTER DELETE ON emails BEGIN
|
|
412
|
+
INSERT INTO emails_fts(emails_fts, rowid, subject, body_text)
|
|
413
|
+
VALUES ('delete', old.rowid, old.subject, old.body_text);
|
|
414
|
+
END;
|
|
415
|
+
|
|
416
|
+
CREATE TRIGGER IF NOT EXISTS emails_au AFTER UPDATE ON emails BEGIN
|
|
417
|
+
INSERT INTO emails_fts(emails_fts, rowid, subject, body_text)
|
|
418
|
+
VALUES ('delete', old.rowid, old.subject, old.body_text);
|
|
419
|
+
INSERT INTO emails_fts(rowid, subject, body_text)
|
|
420
|
+
VALUES (new.rowid, new.subject, new.body_text);
|
|
421
|
+
END;
|
|
422
|
+
|
|
423
|
+
INSERT INTO emails_fts(emails_fts) VALUES ('rebuild');
|
|
424
|
+
|
|
425
|
+
INSERT INTO schema_version (version) VALUES (2);
|
|
426
|
+
`)
|
|
427
|
+
|
|
428
|
+
console.log('[DB] Migration 2 complete: emails FTS5 added')
|
|
429
|
+
} catch (err) {
|
|
430
|
+
console.warn(`[DB] Migration 2 skipped (emails table may not exist yet): ${err.message}`)
|
|
431
|
+
// Mark as applied so it doesn't block subsequent migrations.
|
|
432
|
+
// emails FTS will be created by initSchema on next fresh DB.
|
|
433
|
+
try { db.exec("INSERT OR IGNORE INTO schema_version (version) VALUES (2)") } catch {}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
408
436
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
END;
|
|
437
|
+
if (currentVersion < 3) {
|
|
438
|
+
try {
|
|
439
|
+
console.log('[DB] Migration 3: Adding project_id to memories...')
|
|
413
440
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
VALUES ('delete', old.rowid, old.subject, old.body_text);
|
|
417
|
-
INSERT INTO emails_fts(rowid, subject, body_text)
|
|
418
|
-
VALUES (new.rowid, new.subject, new.body_text);
|
|
419
|
-
END;
|
|
441
|
+
db.exec(`
|
|
442
|
+
ALTER TABLE memories ADD COLUMN project_id TEXT DEFAULT NULL;
|
|
420
443
|
|
|
421
|
-
|
|
444
|
+
CREATE INDEX IF NOT EXISTS idx_memories_project_id ON memories(project_id);
|
|
422
445
|
|
|
423
|
-
|
|
424
|
-
|
|
446
|
+
INSERT INTO schema_version (version) VALUES (3);
|
|
447
|
+
`)
|
|
425
448
|
|
|
426
|
-
|
|
449
|
+
console.log('[DB] Migration 3 complete: memories.project_id added')
|
|
450
|
+
} catch (err) {
|
|
451
|
+
// Column may already exist (duplicate column error)
|
|
452
|
+
console.warn(`[DB] Migration 3 skipped: ${err.message}`)
|
|
453
|
+
try { db.exec("INSERT OR IGNORE INTO schema_version (version) VALUES (3)") } catch {}
|
|
454
|
+
}
|
|
427
455
|
}
|
|
428
456
|
|
|
429
|
-
if (currentVersion <
|
|
430
|
-
|
|
457
|
+
if (currentVersion < 4) {
|
|
458
|
+
try {
|
|
459
|
+
console.log('[DB] Migration 4: Adding workspace_id to chat_sessions...')
|
|
431
460
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
-- NULL = universal memory (cross-project knowledge).
|
|
435
|
-
-- Set = project-scoped memory.
|
|
436
|
-
ALTER TABLE memories ADD COLUMN project_id TEXT DEFAULT NULL;
|
|
461
|
+
db.exec(`
|
|
462
|
+
ALTER TABLE chat_sessions ADD COLUMN workspace_id TEXT DEFAULT NULL;
|
|
437
463
|
|
|
438
|
-
|
|
464
|
+
CREATE INDEX IF NOT EXISTS idx_chat_sessions_workspace
|
|
465
|
+
ON chat_sessions(workspace_id, updated_at DESC);
|
|
439
466
|
|
|
440
|
-
|
|
441
|
-
|
|
467
|
+
INSERT INTO schema_version (version) VALUES (4);
|
|
468
|
+
`)
|
|
442
469
|
|
|
443
|
-
|
|
470
|
+
console.log('[DB] Migration 4 complete: chat_sessions.workspace_id added')
|
|
471
|
+
} catch (err) {
|
|
472
|
+
console.warn(`[DB] Migration 4 skipped: ${err.message}`)
|
|
473
|
+
try { db.exec("INSERT OR IGNORE INTO schema_version (version) VALUES (4)") } catch {}
|
|
474
|
+
}
|
|
444
475
|
}
|
|
445
476
|
}
|
|
446
477
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Chat Session Store (SQLite)
|
|
3
3
|
* Persists chat sessions and messages.
|
|
4
|
-
* Supports multiple sessions
|
|
4
|
+
* Supports multiple sessions scoped by workspace_id.
|
|
5
5
|
* Claude CLI manages conversation context via --resume.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -10,14 +10,22 @@ const { getDb } = require('../db')
|
|
|
10
10
|
const MAX_MESSAGES = 100
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Load the active (most recent) chat session
|
|
13
|
+
* Load the active (most recent) chat session for a workspace.
|
|
14
|
+
* @param {string|null} workspaceId - Workspace ID to scope sessions
|
|
14
15
|
* @returns {object|null} Session object or null if none exists
|
|
15
16
|
*/
|
|
16
|
-
function load() {
|
|
17
|
+
function load(workspaceId) {
|
|
17
18
|
const db = getDb()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
let session
|
|
20
|
+
if (workspaceId) {
|
|
21
|
+
session = db.prepare(
|
|
22
|
+
'SELECT * FROM chat_sessions WHERE workspace_id = ? ORDER BY updated_at DESC LIMIT 1'
|
|
23
|
+
).get(workspaceId)
|
|
24
|
+
} else {
|
|
25
|
+
session = db.prepare(
|
|
26
|
+
'SELECT * FROM chat_sessions WHERE workspace_id IS NULL ORDER BY updated_at DESC LIMIT 1'
|
|
27
|
+
).get()
|
|
28
|
+
}
|
|
21
29
|
if (!session) return null
|
|
22
30
|
|
|
23
31
|
const messages = db.prepare(
|
|
@@ -26,6 +34,7 @@ function load() {
|
|
|
26
34
|
|
|
27
35
|
return {
|
|
28
36
|
session_id: session.session_id,
|
|
37
|
+
workspace_id: session.workspace_id,
|
|
29
38
|
messages,
|
|
30
39
|
turn_count: session.turn_count,
|
|
31
40
|
created_at: session.created_at,
|
|
@@ -49,6 +58,7 @@ function loadById(sessionId) {
|
|
|
49
58
|
|
|
50
59
|
return {
|
|
51
60
|
session_id: session.session_id,
|
|
61
|
+
workspace_id: session.workspace_id,
|
|
52
62
|
messages,
|
|
53
63
|
turn_count: session.turn_count,
|
|
54
64
|
created_at: session.created_at,
|
|
@@ -57,15 +67,32 @@ function loadById(sessionId) {
|
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
/**
|
|
60
|
-
* List
|
|
70
|
+
* List sessions, optionally filtered by workspace_id.
|
|
61
71
|
* @param {number} limit
|
|
62
|
-
* @
|
|
72
|
+
* @param {string|null} workspaceId - If provided, filter by workspace
|
|
73
|
+
* @returns {Array<{ session_id, workspace_id, turn_count, message_count, created_at, updated_at }>}
|
|
63
74
|
*/
|
|
64
|
-
function listSessions(limit = 50) {
|
|
75
|
+
function listSessions(limit = 50, workspaceId) {
|
|
65
76
|
const db = getDb()
|
|
77
|
+
if (workspaceId) {
|
|
78
|
+
return db.prepare(`
|
|
79
|
+
SELECT
|
|
80
|
+
s.session_id,
|
|
81
|
+
s.workspace_id,
|
|
82
|
+
s.turn_count,
|
|
83
|
+
s.created_at,
|
|
84
|
+
s.updated_at,
|
|
85
|
+
(SELECT COUNT(*) FROM chat_messages WHERE session_id = s.session_id) as message_count
|
|
86
|
+
FROM chat_sessions s
|
|
87
|
+
WHERE s.workspace_id = ?
|
|
88
|
+
ORDER BY s.updated_at DESC
|
|
89
|
+
LIMIT ?
|
|
90
|
+
`).all(workspaceId, limit)
|
|
91
|
+
}
|
|
66
92
|
return db.prepare(`
|
|
67
93
|
SELECT
|
|
68
94
|
s.session_id,
|
|
95
|
+
s.workspace_id,
|
|
69
96
|
s.turn_count,
|
|
70
97
|
s.created_at,
|
|
71
98
|
s.updated_at,
|
|
@@ -88,8 +115,8 @@ function save(session) {
|
|
|
88
115
|
|
|
89
116
|
// Insert session
|
|
90
117
|
db.prepare(
|
|
91
|
-
'INSERT INTO chat_sessions (session_id, turn_count, created_at, updated_at) VALUES (?, ?, ?, ?)'
|
|
92
|
-
).run(session.session_id, session.turn_count || 0, session.created_at, session.updated_at)
|
|
118
|
+
'INSERT INTO chat_sessions (session_id, workspace_id, turn_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?)'
|
|
119
|
+
).run(session.session_id, session.workspace_id || null, session.turn_count || 0, session.created_at, session.updated_at)
|
|
93
120
|
|
|
94
121
|
// Insert messages
|
|
95
122
|
const insertMsg = db.prepare(
|
|
@@ -103,14 +130,15 @@ function save(session) {
|
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
/**
|
|
106
|
-
* Add a message to
|
|
107
|
-
* Creates a new session if none exists
|
|
133
|
+
* Add a message to a session.
|
|
134
|
+
* Creates a new session if none exists for the given session_id.
|
|
108
135
|
* Past sessions are preserved (not deleted).
|
|
109
136
|
* @param {string} sessionId - Claude CLI session ID
|
|
110
137
|
* @param {{ role: string, content: string }} msg - Message to add
|
|
111
138
|
* @param {number} [turnCount] - Optional turn count to add
|
|
139
|
+
* @param {string|null} [workspaceId] - Workspace ID for new sessions
|
|
112
140
|
*/
|
|
113
|
-
function addMessage(sessionId, msg, turnCount) {
|
|
141
|
+
function addMessage(sessionId, msg, turnCount, workspaceId) {
|
|
114
142
|
const db = getDb()
|
|
115
143
|
const existing = db.prepare('SELECT * FROM chat_sessions WHERE session_id = ?').get(sessionId)
|
|
116
144
|
|
|
@@ -118,8 +146,8 @@ function addMessage(sessionId, msg, turnCount) {
|
|
|
118
146
|
if (!existing) {
|
|
119
147
|
const now = Date.now()
|
|
120
148
|
db.prepare(
|
|
121
|
-
'INSERT INTO chat_sessions (session_id, turn_count, created_at, updated_at) VALUES (?, ?, ?, ?)'
|
|
122
|
-
).run(sessionId, 0, now, now)
|
|
149
|
+
'INSERT INTO chat_sessions (session_id, workspace_id, turn_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?)'
|
|
150
|
+
).run(sessionId, workspaceId || null, 0, now, now)
|
|
123
151
|
}
|
|
124
152
|
|
|
125
153
|
// Insert message
|
|
@@ -149,13 +177,21 @@ function addMessage(sessionId, msg, turnCount) {
|
|
|
149
177
|
}
|
|
150
178
|
|
|
151
179
|
/**
|
|
152
|
-
* Clear the active session
|
|
153
|
-
*
|
|
154
|
-
* For reset flows, use clearActive() which preserves the session in history.
|
|
180
|
+
* Clear the active session for a workspace.
|
|
181
|
+
* @param {string|null} workspaceId - Workspace to clear session for
|
|
155
182
|
*/
|
|
156
|
-
function clear() {
|
|
183
|
+
function clear(workspaceId) {
|
|
157
184
|
const db = getDb()
|
|
158
|
-
|
|
185
|
+
let active
|
|
186
|
+
if (workspaceId) {
|
|
187
|
+
active = db.prepare(
|
|
188
|
+
'SELECT session_id FROM chat_sessions WHERE workspace_id = ? ORDER BY updated_at DESC LIMIT 1'
|
|
189
|
+
).get(workspaceId)
|
|
190
|
+
} else {
|
|
191
|
+
active = db.prepare(
|
|
192
|
+
'SELECT session_id FROM chat_sessions WHERE workspace_id IS NULL ORDER BY updated_at DESC LIMIT 1'
|
|
193
|
+
).get()
|
|
194
|
+
}
|
|
159
195
|
if (active) {
|
|
160
196
|
db.prepare('DELETE FROM chat_sessions WHERE session_id = ?').run(active.session_id)
|
|
161
197
|
}
|
package/linux/routes/chat.js
CHANGED
|
@@ -41,20 +41,22 @@ async function chatRoutes(fastify) {
|
|
|
41
41
|
return { success: false, error: 'Unauthorized' }
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const { message, session_id, context } = request.body || {}
|
|
44
|
+
const { message, session_id, context, workspace_id } = request.body || {}
|
|
45
45
|
|
|
46
46
|
if (!message || typeof message !== 'string') {
|
|
47
47
|
reply.code(400)
|
|
48
48
|
return { success: false, error: 'message is required' }
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
const workspaceId = workspace_id || null
|
|
52
|
+
|
|
51
53
|
// Build prompt — add memory context on new sessions + page context
|
|
52
54
|
const prompt = await buildContextPrefix(message, context, session_id)
|
|
53
55
|
|
|
54
56
|
// Store user message
|
|
55
57
|
const currentSessionId = session_id || null
|
|
56
58
|
if (currentSessionId) {
|
|
57
|
-
await chatStore.addMessage(currentSessionId, { role: 'user', content: message })
|
|
59
|
+
await chatStore.addMessage(currentSessionId, { role: 'user', content: message }, undefined, workspaceId)
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
// Take over response handling from Fastify for SSE streaming
|
|
@@ -68,7 +70,7 @@ async function chatRoutes(fastify) {
|
|
|
68
70
|
reply.raw.flushHeaders()
|
|
69
71
|
|
|
70
72
|
try {
|
|
71
|
-
await streamLlmResponse(reply.raw, prompt, currentSessionId)
|
|
73
|
+
await streamLlmResponse(reply.raw, prompt, currentSessionId, workspaceId, message)
|
|
72
74
|
} catch (err) {
|
|
73
75
|
console.error('[Chat] stream error:', err.message)
|
|
74
76
|
const errorEvent = JSON.stringify({ type: 'error', error: err.message })
|
|
@@ -78,14 +80,15 @@ async function chatRoutes(fastify) {
|
|
|
78
80
|
reply.raw.end()
|
|
79
81
|
})
|
|
80
82
|
|
|
81
|
-
// GET /api/chat/session - Get active chat session
|
|
83
|
+
// GET /api/chat/session - Get active chat session for a workspace
|
|
82
84
|
fastify.get('/api/chat/session', async (request, reply) => {
|
|
83
85
|
if (!verifyToken(request)) {
|
|
84
86
|
reply.code(401)
|
|
85
87
|
return { success: false, error: 'Unauthorized' }
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
const
|
|
90
|
+
const workspaceId = request.query?.workspace_id || null
|
|
91
|
+
const session = chatStore.load(workspaceId)
|
|
89
92
|
if (!session) {
|
|
90
93
|
return { success: true, session: null }
|
|
91
94
|
}
|
|
@@ -94,6 +97,7 @@ async function chatRoutes(fastify) {
|
|
|
94
97
|
success: true,
|
|
95
98
|
session: {
|
|
96
99
|
session_id: session.session_id,
|
|
100
|
+
workspace_id: session.workspace_id,
|
|
97
101
|
messages: session.messages,
|
|
98
102
|
turn_count: session.turn_count,
|
|
99
103
|
created_at: session.created_at,
|
|
@@ -102,7 +106,7 @@ async function chatRoutes(fastify) {
|
|
|
102
106
|
}
|
|
103
107
|
})
|
|
104
108
|
|
|
105
|
-
// GET /api/chat/sessions - List
|
|
109
|
+
// GET /api/chat/sessions - List sessions, optionally filtered by workspace
|
|
106
110
|
fastify.get('/api/chat/sessions', async (request, reply) => {
|
|
107
111
|
if (!verifyToken(request)) {
|
|
108
112
|
reply.code(401)
|
|
@@ -110,7 +114,8 @@ async function chatRoutes(fastify) {
|
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
const limit = parseInt(request.query?.limit) || 50
|
|
113
|
-
const
|
|
117
|
+
const workspaceId = request.query?.workspace_id || undefined
|
|
118
|
+
const sessions = chatStore.listSessions(limit, workspaceId)
|
|
114
119
|
return { success: true, sessions }
|
|
115
120
|
})
|
|
116
121
|
|
|
@@ -130,14 +135,15 @@ async function chatRoutes(fastify) {
|
|
|
130
135
|
return { success: true, session }
|
|
131
136
|
})
|
|
132
137
|
|
|
133
|
-
// POST /api/chat/clear - Clear the active session
|
|
138
|
+
// POST /api/chat/clear - Clear the active session for a workspace
|
|
134
139
|
fastify.post('/api/chat/clear', async (request, reply) => {
|
|
135
140
|
if (!verifyToken(request)) {
|
|
136
141
|
reply.code(401)
|
|
137
142
|
return { success: false, error: 'Unauthorized' }
|
|
138
143
|
}
|
|
139
144
|
|
|
140
|
-
|
|
145
|
+
const workspaceId = request.body?.workspace_id || null
|
|
146
|
+
await chatStore.clear(workspaceId)
|
|
141
147
|
return { success: true }
|
|
142
148
|
})
|
|
143
149
|
|
|
@@ -168,42 +174,57 @@ async function chatRoutes(fastify) {
|
|
|
168
174
|
return { success: true }
|
|
169
175
|
})
|
|
170
176
|
|
|
171
|
-
// POST /api/chat/reset -
|
|
177
|
+
// POST /api/chat/reset - Carry over relevant messages and start fresh session
|
|
172
178
|
fastify.post('/api/chat/reset', async (request, reply) => {
|
|
173
179
|
if (!verifyToken(request)) {
|
|
174
180
|
reply.code(401)
|
|
175
181
|
return { success: false, error: 'Unauthorized' }
|
|
176
182
|
}
|
|
177
183
|
|
|
178
|
-
const
|
|
184
|
+
const workspaceId = request.body?.workspace_id || null
|
|
185
|
+
const session = await chatStore.load(workspaceId)
|
|
179
186
|
if (!session || session.messages.length === 0) {
|
|
180
|
-
await chatStore.clear()
|
|
181
|
-
return { success: true,
|
|
187
|
+
await chatStore.clear(workspaceId)
|
|
188
|
+
return { success: true, carry_over: null }
|
|
182
189
|
}
|
|
183
190
|
|
|
184
|
-
//
|
|
191
|
+
// Take recent messages for topic analysis
|
|
185
192
|
const recentMessages = session.messages.slice(-20)
|
|
186
|
-
|
|
187
|
-
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 500)}`)
|
|
188
|
-
.join('\n\n')
|
|
189
|
-
|
|
190
|
-
const summarizePrompt = `以下の会話を要約してください。重要なコンテキスト、決定事項、進行中のタスクを含めてください。200文字以内で。\n\n${conversationText}`
|
|
193
|
+
let carryOverMessages = recentMessages
|
|
191
194
|
|
|
192
|
-
|
|
195
|
+
// Ask LLM to find where the current topic begins
|
|
193
196
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
console.error('[Chat] summarization failed:', err.message)
|
|
197
|
-
// Fallback: use last few messages as summary
|
|
198
|
-
const fallback = recentMessages.slice(-4)
|
|
199
|
-
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 200)}`)
|
|
197
|
+
const indexed = recentMessages
|
|
198
|
+
.map((m, i) => `[${i}] ${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 300)}`)
|
|
200
199
|
.join('\n')
|
|
201
|
-
|
|
200
|
+
|
|
201
|
+
const analyzePrompt = `以下の会話履歴を分析し、現在進行中のトピックが始まったインデックス番号を特定してください。
|
|
202
|
+
会話の流れが明確に変わったポイント(新しい質問・別の作業への切り替え等)がある場合、最後の切り替え以降のインデックスを返してください。
|
|
203
|
+
全体が同じトピックの場合は 0 を返してください。
|
|
204
|
+
|
|
205
|
+
数値のみを返してください(例: 5)。
|
|
206
|
+
|
|
207
|
+
${indexed}`
|
|
208
|
+
|
|
209
|
+
const result = await runQuickLlmCall(analyzePrompt)
|
|
210
|
+
const breakIndex = parseInt(result.trim(), 10)
|
|
211
|
+
if (!isNaN(breakIndex) && breakIndex >= 0 && breakIndex < recentMessages.length) {
|
|
212
|
+
carryOverMessages = recentMessages.slice(breakIndex)
|
|
213
|
+
console.log(`[Chat] topic break at index ${breakIndex}, carrying over ${carryOverMessages.length}/${recentMessages.length} messages`)
|
|
214
|
+
}
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error('[Chat] topic analysis failed, carrying over last 10:', err.message)
|
|
217
|
+
carryOverMessages = recentMessages.slice(-10)
|
|
202
218
|
}
|
|
203
219
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
220
|
+
// Format carry-over as conversation text
|
|
221
|
+
const carryOver = carryOverMessages
|
|
222
|
+
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content}`)
|
|
223
|
+
.join('\n\n')
|
|
224
|
+
|
|
225
|
+
await chatStore.clear(workspaceId)
|
|
226
|
+
console.log(`[Chat] session reset, carrying over ${carryOverMessages.length} messages (${carryOver.length} chars)`)
|
|
227
|
+
return { success: true, carry_over: carryOver }
|
|
207
228
|
})
|
|
208
229
|
|
|
209
230
|
// POST /api/chat/end-of-day - Generate daily log + extract memories
|
|
@@ -341,7 +362,7 @@ function getLlmBinary() {
|
|
|
341
362
|
* Tracks block types to correctly forward tool_use vs text events
|
|
342
363
|
* and counts turns for session management.
|
|
343
364
|
*/
|
|
344
|
-
function streamLlmResponse(res, prompt, sessionId) {
|
|
365
|
+
function streamLlmResponse(res, prompt, sessionId, workspaceId, originalMessage) {
|
|
345
366
|
return new Promise((resolve, reject) => {
|
|
346
367
|
const binaryName = getLlmBinary()
|
|
347
368
|
if (!binaryName) {
|
|
@@ -516,11 +537,11 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
516
537
|
if (resolvedSessionId) {
|
|
517
538
|
// If this was a new session, also store the user message now
|
|
518
539
|
if (!sessionId) {
|
|
519
|
-
await chatStore.addMessage(resolvedSessionId, { role: 'user', content: prompt })
|
|
540
|
+
await chatStore.addMessage(resolvedSessionId, { role: 'user', content: originalMessage || prompt }, undefined, workspaceId)
|
|
520
541
|
}
|
|
521
542
|
// Store assistant response with turn count
|
|
522
543
|
if (fullResponse) {
|
|
523
|
-
await chatStore.addMessage(resolvedSessionId, { role: 'assistant', content: fullResponse }, turnCount)
|
|
544
|
+
await chatStore.addMessage(resolvedSessionId, { role: 'assistant', content: fullResponse }, turnCount, workspaceId)
|
|
524
545
|
}
|
|
525
546
|
}
|
|
526
547
|
|
|
@@ -533,7 +554,7 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
533
554
|
}
|
|
534
555
|
|
|
535
556
|
// Load current turn count from session for the done event
|
|
536
|
-
const session = await chatStore.load()
|
|
557
|
+
const session = await chatStore.load(workspaceId)
|
|
537
558
|
const totalTurnCount = session?.turn_count || turnCount
|
|
538
559
|
|
|
539
560
|
const doneEvent = JSON.stringify({
|
package/package.json
CHANGED
package/win/routes/chat.js
CHANGED
|
@@ -116,17 +116,18 @@ async function chatRoutes(fastify) {
|
|
|
116
116
|
return { success: false, error: 'Unauthorized' }
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const { message, session_id, context, wsl_mode } = request.body || {}
|
|
119
|
+
const { message, session_id, context, wsl_mode, workspace_id } = request.body || {}
|
|
120
120
|
if (!message || typeof message !== 'string') {
|
|
121
121
|
reply.code(400)
|
|
122
122
|
return { success: false, error: 'message is required' }
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
const workspaceId = workspace_id || null
|
|
125
126
|
const prompt = await buildContextPrefix(message, context, session_id)
|
|
126
127
|
const currentSessionId = session_id || null
|
|
127
128
|
|
|
128
129
|
if (currentSessionId) {
|
|
129
|
-
await chatStore.addMessage(currentSessionId, { role: 'user', content: message })
|
|
130
|
+
await chatStore.addMessage(currentSessionId, { role: 'user', content: message }, undefined, workspaceId)
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
reply.hijack()
|
|
@@ -144,7 +145,7 @@ async function chatRoutes(fastify) {
|
|
|
144
145
|
await proxyWslChat(reply.raw, prompt, currentSessionId)
|
|
145
146
|
wslModeActive = false
|
|
146
147
|
} else {
|
|
147
|
-
await streamLlmResponse(reply.raw, prompt, currentSessionId)
|
|
148
|
+
await streamLlmResponse(reply.raw, prompt, currentSessionId, workspaceId, message)
|
|
148
149
|
}
|
|
149
150
|
} catch (err) {
|
|
150
151
|
wslModeActive = false
|
|
@@ -160,12 +161,14 @@ async function chatRoutes(fastify) {
|
|
|
160
161
|
reply.code(401)
|
|
161
162
|
return { success: false, error: 'Unauthorized' }
|
|
162
163
|
}
|
|
163
|
-
const
|
|
164
|
+
const workspaceId = request.query?.workspace_id || null
|
|
165
|
+
const session = chatStore.load(workspaceId)
|
|
164
166
|
if (!session) return { success: true, session: null }
|
|
165
167
|
return {
|
|
166
168
|
success: true,
|
|
167
169
|
session: {
|
|
168
170
|
session_id: session.session_id,
|
|
171
|
+
workspace_id: session.workspace_id,
|
|
169
172
|
messages: session.messages,
|
|
170
173
|
turn_count: session.turn_count,
|
|
171
174
|
created_at: session.created_at,
|
|
@@ -174,7 +177,7 @@ async function chatRoutes(fastify) {
|
|
|
174
177
|
}
|
|
175
178
|
})
|
|
176
179
|
|
|
177
|
-
// GET /api/chat/sessions - List
|
|
180
|
+
// GET /api/chat/sessions - List sessions, optionally filtered by workspace
|
|
178
181
|
fastify.get('/api/chat/sessions', async (request, reply) => {
|
|
179
182
|
if (!verifyToken(request)) {
|
|
180
183
|
reply.code(401)
|
|
@@ -182,7 +185,8 @@ async function chatRoutes(fastify) {
|
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
const limit = parseInt(request.query?.limit) || 50
|
|
185
|
-
const
|
|
188
|
+
const workspaceId = request.query?.workspace_id || undefined
|
|
189
|
+
const sessions = chatStore.listSessions(limit, workspaceId)
|
|
186
190
|
return { success: true, sessions }
|
|
187
191
|
})
|
|
188
192
|
|
|
@@ -207,7 +211,8 @@ async function chatRoutes(fastify) {
|
|
|
207
211
|
reply.code(401)
|
|
208
212
|
return { success: false, error: 'Unauthorized' }
|
|
209
213
|
}
|
|
210
|
-
|
|
214
|
+
const workspaceId = request.body?.workspace_id || null
|
|
215
|
+
await chatStore.clear(workspaceId)
|
|
211
216
|
return { success: true }
|
|
212
217
|
})
|
|
213
218
|
|
|
@@ -239,40 +244,57 @@ async function chatRoutes(fastify) {
|
|
|
239
244
|
return { success: true }
|
|
240
245
|
})
|
|
241
246
|
|
|
242
|
-
// POST /api/chat/reset -
|
|
247
|
+
// POST /api/chat/reset - Carry over relevant messages and start fresh session
|
|
243
248
|
fastify.post('/api/chat/reset', async (request, reply) => {
|
|
244
249
|
if (!verifyToken(request)) {
|
|
245
250
|
reply.code(401)
|
|
246
251
|
return { success: false, error: 'Unauthorized' }
|
|
247
252
|
}
|
|
248
253
|
|
|
249
|
-
const
|
|
254
|
+
const workspaceId = request.body?.workspace_id || null
|
|
255
|
+
const session = await chatStore.load(workspaceId)
|
|
250
256
|
if (!session || session.messages.length === 0) {
|
|
251
|
-
await chatStore.clear()
|
|
252
|
-
return { success: true,
|
|
257
|
+
await chatStore.clear(workspaceId)
|
|
258
|
+
return { success: true, carry_over: null }
|
|
253
259
|
}
|
|
254
260
|
|
|
261
|
+
// Take recent messages for topic analysis
|
|
255
262
|
const recentMessages = session.messages.slice(-20)
|
|
256
|
-
|
|
257
|
-
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 500)}`)
|
|
258
|
-
.join('\n\n')
|
|
259
|
-
|
|
260
|
-
const summarizePrompt = `以下の会話を要約してください。重要なコンテキスト、決定事項、進行中のタスクを含めてください。200文字以内で。\n\n${conversationText}`
|
|
263
|
+
let carryOverMessages = recentMessages
|
|
261
264
|
|
|
262
|
-
|
|
265
|
+
// Ask LLM to find where the current topic begins
|
|
263
266
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
console.error('[Chat] summarization failed:', err.message)
|
|
267
|
-
const fallback = recentMessages.slice(-4)
|
|
268
|
-
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 200)}`)
|
|
267
|
+
const indexed = recentMessages
|
|
268
|
+
.map((m, i) => `[${i}] ${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 300)}`)
|
|
269
269
|
.join('\n')
|
|
270
|
-
|
|
270
|
+
|
|
271
|
+
const analyzePrompt = `以下の会話履歴を分析し、現在進行中のトピックが始まったインデックス番号を特定してください。
|
|
272
|
+
会話の流れが明確に変わったポイント(新しい質問・別の作業への切り替え等)がある場合、最後の切り替え以降のインデックスを返してください。
|
|
273
|
+
全体が同じトピックの場合は 0 を返してください。
|
|
274
|
+
|
|
275
|
+
数値のみを返してください(例: 5)。
|
|
276
|
+
|
|
277
|
+
${indexed}`
|
|
278
|
+
|
|
279
|
+
const result = await runQuickLlmCall(analyzePrompt)
|
|
280
|
+
const breakIndex = parseInt(result.trim(), 10)
|
|
281
|
+
if (!isNaN(breakIndex) && breakIndex >= 0 && breakIndex < recentMessages.length) {
|
|
282
|
+
carryOverMessages = recentMessages.slice(breakIndex)
|
|
283
|
+
console.log(`[Chat] topic break at index ${breakIndex}, carrying over ${carryOverMessages.length}/${recentMessages.length} messages`)
|
|
284
|
+
}
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.error('[Chat] topic analysis failed, carrying over last 10:', err.message)
|
|
287
|
+
carryOverMessages = recentMessages.slice(-10)
|
|
271
288
|
}
|
|
272
289
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
290
|
+
// Format carry-over as conversation text
|
|
291
|
+
const carryOver = carryOverMessages
|
|
292
|
+
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content}`)
|
|
293
|
+
.join('\n\n')
|
|
294
|
+
|
|
295
|
+
await chatStore.clear(workspaceId)
|
|
296
|
+
console.log(`[Chat] session reset, carrying over ${carryOverMessages.length} messages (${carryOver.length} chars)`)
|
|
297
|
+
return { success: true, carry_over: carryOver }
|
|
276
298
|
})
|
|
277
299
|
|
|
278
300
|
// POST /api/chat/end-of-day - Generate daily log + extract memories
|
|
@@ -394,7 +416,7 @@ function getLlmBinary() {
|
|
|
394
416
|
* Tracks block types to correctly forward tool_use vs text events
|
|
395
417
|
* and counts turns for session management.
|
|
396
418
|
*/
|
|
397
|
-
function streamLlmResponse(res, prompt, sessionId) {
|
|
419
|
+
function streamLlmResponse(res, prompt, sessionId, workspaceId, originalMessage) {
|
|
398
420
|
return new Promise((resolve, reject) => {
|
|
399
421
|
const binaryName = getLlmBinary()
|
|
400
422
|
if (!binaryName) {
|
|
@@ -542,10 +564,10 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
542
564
|
activeChatChild = null
|
|
543
565
|
if (resolvedSessionId) {
|
|
544
566
|
if (!sessionId) {
|
|
545
|
-
await chatStore.addMessage(resolvedSessionId, { role: 'user', content: prompt })
|
|
567
|
+
await chatStore.addMessage(resolvedSessionId, { role: 'user', content: originalMessage || prompt }, undefined, workspaceId)
|
|
546
568
|
}
|
|
547
569
|
if (fullResponse) {
|
|
548
|
-
await chatStore.addMessage(resolvedSessionId, { role: 'assistant', content: fullResponse }, turnCount)
|
|
570
|
+
await chatStore.addMessage(resolvedSessionId, { role: 'assistant', content: fullResponse }, turnCount, workspaceId)
|
|
549
571
|
}
|
|
550
572
|
}
|
|
551
573
|
if (code !== 0 && !fullResponse) {
|
|
@@ -553,7 +575,7 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
553
575
|
res.write(`data: ${JSON.stringify({ type: 'error', error: errorMsg })}\n\n`)
|
|
554
576
|
}
|
|
555
577
|
|
|
556
|
-
const session = await chatStore.load()
|
|
578
|
+
const session = await chatStore.load(workspaceId)
|
|
557
579
|
const totalTurnCount = session?.turn_count || turnCount
|
|
558
580
|
|
|
559
581
|
res.write(`data: ${JSON.stringify({ type: 'done', session_id: resolvedSessionId, turn_count: totalTurnCount })}\n\n`)
|