@geekbeer/minion 4.2.0 → 4.2.1
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/stores/chat-store.js +63 -31
- package/linux/routes/chat.js +61 -26
- package/package.json +1 -1
- package/win/routes/chat.js +80 -35
|
@@ -133,6 +133,7 @@ function save(session) {
|
|
|
133
133
|
* Add a message to a session.
|
|
134
134
|
* Creates a new session if none exists for the given session_id.
|
|
135
135
|
* Past sessions are preserved (not deleted).
|
|
136
|
+
* Wrapped in a transaction so partial saves can't happen on failure.
|
|
136
137
|
* @param {string} sessionId - Claude CLI session ID
|
|
137
138
|
* @param {{ role: string, content: string }} msg - Message to add
|
|
138
139
|
* @param {number} [turnCount] - Optional turn count to add
|
|
@@ -140,40 +141,71 @@ function save(session) {
|
|
|
140
141
|
*/
|
|
141
142
|
function addMessage(sessionId, msg, turnCount, workspaceId) {
|
|
142
143
|
const db = getDb()
|
|
143
|
-
const
|
|
144
|
+
const tx = db.transaction(() => {
|
|
145
|
+
const existing = db.prepare('SELECT session_id FROM chat_sessions WHERE session_id = ?').get(sessionId)
|
|
146
|
+
|
|
147
|
+
if (!existing) {
|
|
148
|
+
const now = Date.now()
|
|
149
|
+
db.prepare(
|
|
150
|
+
'INSERT INTO chat_sessions (session_id, workspace_id, turn_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?)'
|
|
151
|
+
).run(sessionId, workspaceId || null, 0, now, now)
|
|
152
|
+
}
|
|
144
153
|
|
|
145
|
-
// If session doesn't exist, create a new one (past sessions remain in DB)
|
|
146
|
-
if (!existing) {
|
|
147
|
-
const now = Date.now()
|
|
148
154
|
db.prepare(
|
|
149
|
-
'INSERT INTO
|
|
150
|
-
).run(sessionId,
|
|
151
|
-
}
|
|
155
|
+
'INSERT INTO chat_messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)'
|
|
156
|
+
).run(sessionId, msg.role, msg.content, Date.now())
|
|
152
157
|
|
|
153
|
-
|
|
154
|
-
db.prepare(
|
|
155
|
-
'INSERT INTO chat_messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)'
|
|
156
|
-
).run(sessionId, msg.role, msg.content, Date.now())
|
|
157
|
-
|
|
158
|
-
// Update session metadata
|
|
159
|
-
const newTurnCount = (turnCount && turnCount > 0) ? turnCount : 0
|
|
160
|
-
db.prepare(`
|
|
161
|
-
UPDATE chat_sessions
|
|
162
|
-
SET updated_at = ?,
|
|
163
|
-
turn_count = turn_count + ?
|
|
164
|
-
WHERE session_id = ?
|
|
165
|
-
`).run(Date.now(), newTurnCount, sessionId)
|
|
166
|
-
|
|
167
|
-
// Prune old messages for this session (keep only MAX_MESSAGES most recent)
|
|
168
|
-
const count = db.prepare('SELECT COUNT(*) as cnt FROM chat_messages WHERE session_id = ?').get(sessionId).cnt
|
|
169
|
-
if (count > MAX_MESSAGES) {
|
|
158
|
+
const newTurnCount = (turnCount && turnCount > 0) ? turnCount : 0
|
|
170
159
|
db.prepare(`
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
`).run(
|
|
176
|
-
|
|
160
|
+
UPDATE chat_sessions
|
|
161
|
+
SET updated_at = ?,
|
|
162
|
+
turn_count = turn_count + ?
|
|
163
|
+
WHERE session_id = ?
|
|
164
|
+
`).run(Date.now(), newTurnCount, sessionId)
|
|
165
|
+
|
|
166
|
+
const count = db.prepare('SELECT COUNT(*) as cnt FROM chat_messages WHERE session_id = ?').get(sessionId).cnt
|
|
167
|
+
if (count > MAX_MESSAGES) {
|
|
168
|
+
db.prepare(`
|
|
169
|
+
DELETE FROM chat_messages WHERE id IN (
|
|
170
|
+
SELECT id FROM chat_messages WHERE session_id = ?
|
|
171
|
+
ORDER BY id ASC LIMIT ?
|
|
172
|
+
)
|
|
173
|
+
`).run(sessionId, count - MAX_MESSAGES)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
tx()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Rename a session's ID, carrying over all messages.
|
|
181
|
+
* Used when a pending session ID (generated locally before the LLM call) is
|
|
182
|
+
* replaced by the real session_id returned by Claude CLI.
|
|
183
|
+
* If `newSessionId` already exists, messages from `oldSessionId` are merged
|
|
184
|
+
* into it and the old session row is dropped.
|
|
185
|
+
* @param {string} oldSessionId
|
|
186
|
+
* @param {string} newSessionId
|
|
187
|
+
* @returns {boolean} true if any rows were touched
|
|
188
|
+
*/
|
|
189
|
+
function rekeySession(oldSessionId, newSessionId) {
|
|
190
|
+
if (!oldSessionId || !newSessionId || oldSessionId === newSessionId) return false
|
|
191
|
+
const db = getDb()
|
|
192
|
+
const tx = db.transaction(() => {
|
|
193
|
+
const oldSession = db.prepare('SELECT * FROM chat_sessions WHERE session_id = ?').get(oldSessionId)
|
|
194
|
+
if (!oldSession) return false
|
|
195
|
+
|
|
196
|
+
const existingNew = db.prepare('SELECT session_id FROM chat_sessions WHERE session_id = ?').get(newSessionId)
|
|
197
|
+
if (existingNew) {
|
|
198
|
+
db.prepare('UPDATE chat_messages SET session_id = ? WHERE session_id = ?').run(newSessionId, oldSessionId)
|
|
199
|
+
db.prepare('UPDATE chat_sessions SET updated_at = ?, turn_count = turn_count + ? WHERE session_id = ?')
|
|
200
|
+
.run(Date.now(), oldSession.turn_count || 0, newSessionId)
|
|
201
|
+
db.prepare('DELETE FROM chat_sessions WHERE session_id = ?').run(oldSessionId)
|
|
202
|
+
} else {
|
|
203
|
+
db.prepare('UPDATE chat_sessions SET session_id = ? WHERE session_id = ?').run(newSessionId, oldSessionId)
|
|
204
|
+
db.prepare('UPDATE chat_messages SET session_id = ? WHERE session_id = ?').run(newSessionId, oldSessionId)
|
|
205
|
+
}
|
|
206
|
+
return true
|
|
207
|
+
})
|
|
208
|
+
return tx() === true
|
|
177
209
|
}
|
|
178
210
|
|
|
179
211
|
/**
|
|
@@ -208,4 +240,4 @@ function deleteSession(sessionId) {
|
|
|
208
240
|
return result.changes > 0
|
|
209
241
|
}
|
|
210
242
|
|
|
211
|
-
module.exports = { load, loadById, listSessions, save, addMessage, clear, deleteSession }
|
|
243
|
+
module.exports = { load, loadById, listSessions, save, addMessage, rekeySession, clear, deleteSession }
|
package/linux/routes/chat.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
const { spawn } = require('child_process')
|
|
20
|
+
const crypto = require('crypto')
|
|
20
21
|
const fs = require('fs')
|
|
21
22
|
const path = require('path')
|
|
22
23
|
const { verifyToken } = require('../../core/lib/auth')
|
|
@@ -57,10 +58,16 @@ async function chatRoutes(fastify) {
|
|
|
57
58
|
// so the user's chat log keeps just the [task:UUID] tag, not a noisy dump.
|
|
58
59
|
const prompt = await buildContextPrefix(message, context, session_id, workspaceId, referenced_tasks)
|
|
59
60
|
|
|
60
|
-
//
|
|
61
|
+
// Persist the user message BEFORE invoking the LLM so that crashes,
|
|
62
|
+
// timeouts, or unparseable CLI output can't lose it. For new sessions we
|
|
63
|
+
// mint a local pending ID; it gets rekeyed to the real Claude CLI session
|
|
64
|
+
// ID once that comes back on the SSE stream.
|
|
61
65
|
const currentSessionId = session_id || null
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
const pendingSessionId = currentSessionId || `pending-${crypto.randomUUID()}`
|
|
67
|
+
try {
|
|
68
|
+
await chatStore.addMessage(pendingSessionId, { role: 'user', content: message }, undefined, workspaceId)
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error('[Chat] failed to persist user message:', err.message)
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
// Take over response handling from Fastify for SSE streaming
|
|
@@ -74,7 +81,7 @@ async function chatRoutes(fastify) {
|
|
|
74
81
|
reply.raw.flushHeaders()
|
|
75
82
|
|
|
76
83
|
try {
|
|
77
|
-
await streamLlmResponse(reply.raw, prompt, currentSessionId, workspaceId, message)
|
|
84
|
+
await streamLlmResponse(reply.raw, prompt, currentSessionId, workspaceId, message, pendingSessionId)
|
|
78
85
|
} catch (err) {
|
|
79
86
|
console.error('[Chat] stream error:', err.message)
|
|
80
87
|
const errorEvent = JSON.stringify({ type: 'error', error: err.message })
|
|
@@ -541,16 +548,16 @@ function getLlmBinary() {
|
|
|
541
548
|
* Tracks block types to correctly forward tool_use vs text events
|
|
542
549
|
* and counts turns for session management.
|
|
543
550
|
*/
|
|
544
|
-
async function streamLlmResponse(res, prompt, sessionId, workspaceId, originalMessage) {
|
|
551
|
+
async function streamLlmResponse(res, prompt, sessionId, workspaceId, originalMessage, pendingSessionId) {
|
|
545
552
|
// Plugin system path: Primary is set → delegate to plugin
|
|
546
553
|
const primary = getActivePrimary()
|
|
547
554
|
if (primary) {
|
|
548
|
-
return streamViaPlugin(primary, res, prompt, sessionId, workspaceId, originalMessage)
|
|
555
|
+
return streamViaPlugin(primary, res, prompt, sessionId, workspaceId, originalMessage, pendingSessionId)
|
|
549
556
|
}
|
|
550
|
-
return streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, originalMessage)
|
|
557
|
+
return streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, originalMessage, pendingSessionId)
|
|
551
558
|
}
|
|
552
559
|
|
|
553
|
-
async function streamViaPlugin(plugin, res, prompt, sessionId, workspaceId, originalMessage) {
|
|
560
|
+
async function streamViaPlugin(plugin, res, prompt, sessionId, workspaceId, originalMessage, pendingSessionId) {
|
|
554
561
|
const input = { prompt }
|
|
555
562
|
const activeRef = { current: null }
|
|
556
563
|
activeChatChild = { kill: () => activeRef.current?.kill?.('SIGTERM') }
|
|
@@ -576,6 +583,7 @@ async function streamViaPlugin(plugin, res, prompt, sessionId, workspaceId, orig
|
|
|
576
583
|
|
|
577
584
|
res.on('close', () => { activeRef.current?.kill?.('SIGTERM') })
|
|
578
585
|
|
|
586
|
+
let pluginError = null
|
|
579
587
|
try {
|
|
580
588
|
let output
|
|
581
589
|
if (plugin.capabilities.streaming && typeof plugin.stream === 'function') {
|
|
@@ -592,25 +600,42 @@ async function streamViaPlugin(plugin, res, prompt, sessionId, workspaceId, orig
|
|
|
592
600
|
}
|
|
593
601
|
}
|
|
594
602
|
resolvedSessionId = output?.metadata?.sessionId || resolvedSessionId
|
|
603
|
+
} catch (err) {
|
|
604
|
+
// Swallow here so we can persist any partial response first; rethrow below.
|
|
605
|
+
pluginError = err
|
|
595
606
|
} finally {
|
|
596
607
|
activeChatChild = null
|
|
597
608
|
}
|
|
598
609
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
610
|
+
// For new sessions, the user message was persisted under pendingSessionId
|
|
611
|
+
// before the plugin call. Rekey it to the real session ID now that we
|
|
612
|
+
// know it. If the plugin never reported a session ID, leave the message
|
|
613
|
+
// under the pending key so the history isn't lost.
|
|
614
|
+
const persistSessionId = resolvedSessionId || pendingSessionId
|
|
615
|
+
try {
|
|
616
|
+
if (!sessionId && resolvedSessionId && pendingSessionId && pendingSessionId !== resolvedSessionId) {
|
|
617
|
+
chatStore.rekeySession(pendingSessionId, resolvedSessionId)
|
|
602
618
|
}
|
|
603
|
-
if (fullResponse) {
|
|
604
|
-
await chatStore.addMessage(
|
|
619
|
+
if (fullResponse && persistSessionId) {
|
|
620
|
+
await chatStore.addMessage(persistSessionId, { role: 'assistant', content: fullResponse }, turnCount, workspaceId)
|
|
605
621
|
}
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.error('[Chat] failed to persist assistant message:', err.message)
|
|
606
624
|
}
|
|
607
625
|
|
|
608
|
-
|
|
609
|
-
|
|
626
|
+
if (pluginError) throw pluginError
|
|
627
|
+
|
|
628
|
+
let totalTurnCount = turnCount
|
|
629
|
+
try {
|
|
630
|
+
const session = await chatStore.load(workspaceId)
|
|
631
|
+
totalTurnCount = session?.turn_count || turnCount
|
|
632
|
+
} catch (err) {
|
|
633
|
+
console.error('[Chat] failed to load session for done event:', err.message)
|
|
634
|
+
}
|
|
610
635
|
res.write(`data: ${JSON.stringify({ type: 'done', session_id: resolvedSessionId, turn_count: totalTurnCount })}\n\n`)
|
|
611
636
|
}
|
|
612
637
|
|
|
613
|
-
function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, originalMessage) {
|
|
638
|
+
function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, originalMessage, pendingSessionId) {
|
|
614
639
|
return new Promise((resolve, reject) => {
|
|
615
640
|
const binaryName = getLlmBinary()
|
|
616
641
|
if (!binaryName) {
|
|
@@ -786,16 +811,21 @@ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, original
|
|
|
786
811
|
console.log(`[Chat] final stderr (tail 500): ${stderrBuffer.slice(-500)}`)
|
|
787
812
|
}
|
|
788
813
|
|
|
789
|
-
//
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
814
|
+
// For new sessions, the user message was already persisted under
|
|
815
|
+
// pendingSessionId before spawn. Rekey it to the real session ID when
|
|
816
|
+
// Claude CLI reported one; otherwise leave the message under the
|
|
817
|
+
// pending key so the history is never lost on crash.
|
|
818
|
+
const persistSessionId = resolvedSessionId || pendingSessionId
|
|
819
|
+
try {
|
|
820
|
+
if (!sessionId && resolvedSessionId && pendingSessionId && pendingSessionId !== resolvedSessionId) {
|
|
821
|
+
chatStore.rekeySession(pendingSessionId, resolvedSessionId)
|
|
794
822
|
}
|
|
795
|
-
//
|
|
796
|
-
if (fullResponse) {
|
|
797
|
-
await chatStore.addMessage(
|
|
823
|
+
// Persist any partial response we managed to collect, even on error
|
|
824
|
+
if (fullResponse && persistSessionId) {
|
|
825
|
+
await chatStore.addMessage(persistSessionId, { role: 'assistant', content: fullResponse }, turnCount, workspaceId)
|
|
798
826
|
}
|
|
827
|
+
} catch (err) {
|
|
828
|
+
console.error('[Chat] failed to persist assistant message:', err.message)
|
|
799
829
|
}
|
|
800
830
|
|
|
801
831
|
if (code !== 0) {
|
|
@@ -811,8 +841,13 @@ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, original
|
|
|
811
841
|
}
|
|
812
842
|
|
|
813
843
|
// Load current turn count from session for the done event
|
|
814
|
-
|
|
815
|
-
|
|
844
|
+
let totalTurnCount = turnCount
|
|
845
|
+
try {
|
|
846
|
+
const session = await chatStore.load(workspaceId)
|
|
847
|
+
totalTurnCount = session?.turn_count || turnCount
|
|
848
|
+
} catch (err) {
|
|
849
|
+
console.error('[Chat] failed to load session for done event:', err.message)
|
|
850
|
+
}
|
|
816
851
|
|
|
817
852
|
const doneEvent = JSON.stringify({
|
|
818
853
|
type: 'done',
|
package/package.json
CHANGED
package/win/routes/chat.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const { spawn } = require('child_process')
|
|
16
|
+
const crypto = require('crypto')
|
|
16
17
|
const fs = require('fs')
|
|
17
18
|
const path = require('path')
|
|
18
19
|
const http = require('http')
|
|
@@ -130,8 +131,19 @@ async function chatRoutes(fastify) {
|
|
|
130
131
|
const prompt = await buildContextPrefix(message, context, session_id, workspaceId, referenced_tasks)
|
|
131
132
|
const currentSessionId = session_id || null
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
// Persist the user message BEFORE invoking the LLM so that crashes,
|
|
135
|
+
// timeouts, or unparseable CLI output can't lose it. New sessions get a
|
|
136
|
+
// local pending ID; it gets rekeyed to the real Claude CLI session ID
|
|
137
|
+
// once that comes back on the SSE stream.
|
|
138
|
+
// In WSL mode the upstream WSL session server keeps its own store, so we
|
|
139
|
+
// only persist locally for existing sessions (matching prior behavior).
|
|
140
|
+
const pendingSessionId = currentSessionId || `pending-${crypto.randomUUID()}`
|
|
141
|
+
if (!wsl_mode || currentSessionId) {
|
|
142
|
+
try {
|
|
143
|
+
await chatStore.addMessage(pendingSessionId, { role: 'user', content: message }, undefined, workspaceId)
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error('[Chat] failed to persist user message:', err.message)
|
|
146
|
+
}
|
|
135
147
|
}
|
|
136
148
|
|
|
137
149
|
reply.hijack()
|
|
@@ -149,7 +161,7 @@ async function chatRoutes(fastify) {
|
|
|
149
161
|
await proxyWslChat(reply.raw, prompt, currentSessionId)
|
|
150
162
|
wslModeActive = false
|
|
151
163
|
} else {
|
|
152
|
-
await streamLlmResponse(reply.raw, prompt, currentSessionId, workspaceId, message)
|
|
164
|
+
await streamLlmResponse(reply.raw, prompt, currentSessionId, workspaceId, message, pendingSessionId)
|
|
153
165
|
}
|
|
154
166
|
} catch (err) {
|
|
155
167
|
wslModeActive = false
|
|
@@ -594,15 +606,15 @@ function getLlmBinary() {
|
|
|
594
606
|
* Tracks block types to correctly forward tool_use vs text events
|
|
595
607
|
* and counts turns for session management.
|
|
596
608
|
*/
|
|
597
|
-
async function streamLlmResponse(res, prompt, sessionId, workspaceId, originalMessage) {
|
|
609
|
+
async function streamLlmResponse(res, prompt, sessionId, workspaceId, originalMessage, pendingSessionId) {
|
|
598
610
|
const primary = getActivePrimary()
|
|
599
611
|
if (primary) {
|
|
600
|
-
return streamViaPlugin(primary, res, prompt, sessionId, workspaceId, originalMessage)
|
|
612
|
+
return streamViaPlugin(primary, res, prompt, sessionId, workspaceId, originalMessage, pendingSessionId)
|
|
601
613
|
}
|
|
602
|
-
return streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, originalMessage)
|
|
614
|
+
return streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, originalMessage, pendingSessionId)
|
|
603
615
|
}
|
|
604
616
|
|
|
605
|
-
async function streamViaPlugin(plugin, res, prompt, sessionId, workspaceId, originalMessage) {
|
|
617
|
+
async function streamViaPlugin(plugin, res, prompt, sessionId, workspaceId, originalMessage, pendingSessionId) {
|
|
606
618
|
const input = { prompt }
|
|
607
619
|
const activeRef = { current: null }
|
|
608
620
|
|
|
@@ -627,37 +639,57 @@ async function streamViaPlugin(plugin, res, prompt, sessionId, workspaceId, orig
|
|
|
627
639
|
|
|
628
640
|
res.on('close', () => { activeRef.current?.kill?.('SIGTERM') })
|
|
629
641
|
|
|
630
|
-
let
|
|
631
|
-
|
|
632
|
-
output
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
+
let pluginError = null
|
|
643
|
+
try {
|
|
644
|
+
let output
|
|
645
|
+
if (plugin.capabilities.streaming && typeof plugin.stream === 'function') {
|
|
646
|
+
output = await plugin.stream(input, emit, { resumeSessionId: sessionId, activeChildRef: activeRef })
|
|
647
|
+
} else {
|
|
648
|
+
output = await plugin.invoke(input)
|
|
649
|
+
if (output.text) {
|
|
650
|
+
fullResponse = output.text
|
|
651
|
+
res.write(`data: ${JSON.stringify({ type: 'text', content: output.text })}\n\n`)
|
|
652
|
+
turnCount = 1
|
|
653
|
+
}
|
|
654
|
+
if (output.error) {
|
|
655
|
+
res.write(`data: ${JSON.stringify({ type: 'error', error: output.error.message })}\n\n`)
|
|
656
|
+
}
|
|
642
657
|
}
|
|
658
|
+
resolvedSessionId = output?.metadata?.sessionId || resolvedSessionId
|
|
659
|
+
} catch (err) {
|
|
660
|
+
// Swallow here so we can persist any partial response first; rethrow below.
|
|
661
|
+
pluginError = err
|
|
643
662
|
}
|
|
644
|
-
resolvedSessionId = output?.metadata?.sessionId || resolvedSessionId
|
|
645
663
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
664
|
+
// For new sessions, the user message was persisted under pendingSessionId
|
|
665
|
+
// before the plugin call. Rekey it to the real session ID now that we
|
|
666
|
+
// know it. If the plugin never reported a session ID, leave the message
|
|
667
|
+
// under the pending key so the history isn't lost.
|
|
668
|
+
const persistSessionId = resolvedSessionId || pendingSessionId
|
|
669
|
+
try {
|
|
670
|
+
if (!sessionId && resolvedSessionId && pendingSessionId && pendingSessionId !== resolvedSessionId) {
|
|
671
|
+
chatStore.rekeySession(pendingSessionId, resolvedSessionId)
|
|
649
672
|
}
|
|
650
|
-
if (fullResponse) {
|
|
651
|
-
await chatStore.addMessage(
|
|
673
|
+
if (fullResponse && persistSessionId) {
|
|
674
|
+
await chatStore.addMessage(persistSessionId, { role: 'assistant', content: fullResponse }, turnCount, workspaceId)
|
|
652
675
|
}
|
|
676
|
+
} catch (err) {
|
|
677
|
+
console.error('[Chat] failed to persist assistant message:', err.message)
|
|
653
678
|
}
|
|
654
679
|
|
|
655
|
-
|
|
656
|
-
|
|
680
|
+
if (pluginError) throw pluginError
|
|
681
|
+
|
|
682
|
+
let totalTurnCount = turnCount
|
|
683
|
+
try {
|
|
684
|
+
const session = await chatStore.load(workspaceId)
|
|
685
|
+
totalTurnCount = session?.turn_count || turnCount
|
|
686
|
+
} catch (err) {
|
|
687
|
+
console.error('[Chat] failed to load session for done event:', err.message)
|
|
688
|
+
}
|
|
657
689
|
res.write(`data: ${JSON.stringify({ type: 'done', session_id: resolvedSessionId, turn_count: totalTurnCount })}\n\n`)
|
|
658
690
|
}
|
|
659
691
|
|
|
660
|
-
function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, originalMessage) {
|
|
692
|
+
function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, originalMessage, pendingSessionId) {
|
|
661
693
|
return new Promise((resolve, reject) => {
|
|
662
694
|
const binaryName = getLlmBinary()
|
|
663
695
|
if (!binaryName) {
|
|
@@ -809,14 +841,22 @@ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, original
|
|
|
809
841
|
console.log(`[Chat] final stderr (tail 500): ${stderrBuffer.slice(-500)}`)
|
|
810
842
|
}
|
|
811
843
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
844
|
+
// For new sessions, the user message was already persisted under
|
|
845
|
+
// pendingSessionId before spawn. Rekey it to the real session ID when
|
|
846
|
+
// Claude CLI reported one; otherwise leave the message under the
|
|
847
|
+
// pending key so the history is never lost on crash.
|
|
848
|
+
const persistSessionId = resolvedSessionId || pendingSessionId
|
|
849
|
+
try {
|
|
850
|
+
if (!sessionId && resolvedSessionId && pendingSessionId && pendingSessionId !== resolvedSessionId) {
|
|
851
|
+
chatStore.rekeySession(pendingSessionId, resolvedSessionId)
|
|
815
852
|
}
|
|
816
|
-
if (fullResponse) {
|
|
817
|
-
await chatStore.addMessage(
|
|
853
|
+
if (fullResponse && persistSessionId) {
|
|
854
|
+
await chatStore.addMessage(persistSessionId, { role: 'assistant', content: fullResponse }, turnCount, workspaceId)
|
|
818
855
|
}
|
|
856
|
+
} catch (err) {
|
|
857
|
+
console.error('[Chat] failed to persist assistant message:', err.message)
|
|
819
858
|
}
|
|
859
|
+
|
|
820
860
|
if (code !== 0) {
|
|
821
861
|
const errorMsg = stderrBuffer.trim() || `Claude CLI exited with code ${code}`
|
|
822
862
|
console.error(`[Chat] CLI failed (exit ${code}, partial=${!!fullResponse}): ${errorMsg}`)
|
|
@@ -828,8 +868,13 @@ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, original
|
|
|
828
868
|
})}\n\n`)
|
|
829
869
|
}
|
|
830
870
|
|
|
831
|
-
|
|
832
|
-
|
|
871
|
+
let totalTurnCount = turnCount
|
|
872
|
+
try {
|
|
873
|
+
const session = await chatStore.load(workspaceId)
|
|
874
|
+
totalTurnCount = session?.turn_count || turnCount
|
|
875
|
+
} catch (err) {
|
|
876
|
+
console.error('[Chat] failed to load session for done event:', err.message)
|
|
877
|
+
}
|
|
833
878
|
|
|
834
879
|
res.write(`data: ${JSON.stringify({ type: 'done', session_id: resolvedSessionId, turn_count: totalTurnCount })}\n\n`)
|
|
835
880
|
resolve()
|