mirror of
https://github.com/lotsoftick/openclaw_client.git
synced 2026-08-14 00:48:07 +00:00
fix: prevent duplicate assistant messages in DB
Root cause: the chat handler saved assistant messages from JSONL after the stream ended, while the poll endpoint also synced JSONL to DB periodically. Gateway v4 rewrites JSONL entries during multi-tool-call turns, changing the externalId for the same logical message between reads. Each poll saw a 'new' externalId and inserted another copy. Fix: remove assistant message persistence from the chat handler. The poll endpoint already handles JSONL-to-DB sync with robust dedup. The chat handler now only links the user message externalId. Also: API_HOST env var support for binding to Tailscale-only.
This commit is contained in:
+2
-1
@@ -48,7 +48,8 @@ const PORT = Number(process.env.PORT) || 18802;
|
||||
: colors.yellow('[gateway] initial connection failed, will use CLI fallback')
|
||||
);
|
||||
|
||||
const server = app.listen(PORT, () => console.log(colors.green(`running on port ${PORT}`)));
|
||||
const API_HOST = process.env.API_HOST || '0.0.0.0';
|
||||
const server = app.listen(PORT, API_HOST, () => console.log(colors.green(`running on ${API_HOST}:${PORT}`)));
|
||||
attachPtyWebSocket(server);
|
||||
startUpdateChecker();
|
||||
} catch (error) {
|
||||
|
||||
@@ -200,42 +200,19 @@ const chat: Chat = async (req, res, next) => {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// Fetch messages from JSONL to get externalIds
|
||||
// Link the user message to its JSONL externalId so the poll
|
||||
// endpoint can recognise it later and skip duplicate inserts.
|
||||
// Assistant message persistence is left entirely to the poll
|
||||
// endpoint which has robust dedup logic; saving it here as well
|
||||
// caused duplicate rows because the JSONL externalId can shift
|
||||
// between reads (gateway v4 multi-pass writes).
|
||||
try {
|
||||
const jsonlMessages = ocService.getSessionMessages(agentIdForFiles, sessionKey);
|
||||
if (jsonlMessages.length) {
|
||||
const lastUserJsonl = [...jsonlMessages].reverse().find((m) => m.role === 'user');
|
||||
const lastAssistantJsonl = [...jsonlMessages].reverse().find((m) => m.role === 'assistant');
|
||||
|
||||
if (lastUserJsonl?.externalId) {
|
||||
await msgRepo.update(savedUser._id, { externalId: lastUserJsonl.externalId });
|
||||
}
|
||||
|
||||
// Save assistant message from JSONL
|
||||
if (lastAssistantJsonl) {
|
||||
const assistantText = stripWrapperTags(lastAssistantJsonl.text).trim();
|
||||
const assistantThinking = lastAssistantJsonl.thinking
|
||||
? stripWrapperTags(lastAssistantJsonl.thinking).trim()
|
||||
: null;
|
||||
const assistantToolSteps = lastAssistantJsonl.toolSteps ?? null;
|
||||
|
||||
/* Persist if we got any signal: real text, thinking, or tool calls
|
||||
* — the last produces a compact tool-stub bubble in the UI. */
|
||||
if (assistantText || assistantThinking || (assistantToolSteps && assistantToolSteps.length > 0)) {
|
||||
const assistantMessage = msgRepo.create({
|
||||
conversationId: Number(conversationId),
|
||||
externalId: lastAssistantJsonl.externalId || null,
|
||||
text: assistantText,
|
||||
thinking: assistantThinking || null,
|
||||
toolSteps:
|
||||
assistantToolSteps && assistantToolSteps.length > 0 ? assistantToolSteps : null,
|
||||
role: 'assistant' as const,
|
||||
createdBy: req.user!._id,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
await msgRepo.save(assistantMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Non-critical
|
||||
|
||||
Reference in New Issue
Block a user