fix: repair chat history from DB when Goose session has empty placeholders

Portal direct chat → goosed escalation can leave hollow Goose messages while
h5_conversation_messages retains the full dialogue. Backfill session detail
from DB, extend snapshot list fallback, and keep stored session id on transient
boot failures so refresh no longer looks like a blank new chat.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-05 11:04:25 +08:00
parent d3239ff292
commit 614837e199
7 changed files with 371 additions and 9 deletions
+25 -5
View File
@@ -164,6 +164,7 @@ import { createFeedbackService } from './user-feedback.mjs';
import { startScheduleReminderWorker } from './schedule-reminder-worker.mjs';
import { createLlmProviderService, RELAY_BOOTSTRAP } from './llm-providers.mjs';
import { createDirectChatService, isDirectChatSessionId, isPortalDirectChatSnapshot, sendDirectChatSessionEvents, shouldExpirePortalDirectChatSnapshot } from './direct-chat-service.mjs';
import { repairSessionConversationFromDb } from './conversation-repair.mjs';
import { createManagedChatIntentRouter } from './chat-intent-router.mjs';
import { createSessionSnapshotService } from './session-snapshot.mjs';
import { createConversationMemoryService } from './conversation-memory.mjs';
@@ -1888,7 +1889,7 @@ async function ensureUserMemoryCapability(req, res) {
return capabilityState;
}
async function loadUserVisibleConversation(sessionId) {
async function loadUserVisibleConversation(sessionId, userId) {
const target = await tkmindProxy.resolveTarget(sessionId);
const upstream = await tkmindProxy.apiFetchTo(target, `/sessions/${encodeURIComponent(sessionId)}`, {
method: 'GET',
@@ -1897,7 +1898,10 @@ async function loadUserVisibleConversation(sessionId) {
const message = await upstream.text().catch(() => '');
throw new Error(message || '读取会话失败');
}
const session = await upstream.json();
let session = await upstream.json();
if (authPool && userId) {
session = await repairSessionConversationFromDb(authPool, session, sessionId, userId);
}
return (session?.conversation ?? []).filter((message) => message?.metadata?.userVisible);
}
@@ -1928,7 +1932,7 @@ api.post('/user-memory/v1/remember-recent', async (req, res) => {
}
try {
const messages = await loadUserVisibleConversation(sessionId);
const messages = await loadUserVisibleConversation(sessionId, req.currentUser.id);
const result = await memoryV2.write({
userId: req.currentUser.id,
sessionId,
@@ -4469,11 +4473,19 @@ api.get('/sessions/:sessionId', async (req, res, next) => {
req.currentUser,
);
// Cache hit — reconstruct a Goose-compatible session response.
const cachedGooseSession = {
let cachedGooseSession = {
...snapshot.session,
// Embed only userVisible messages so getSession callers still work.
conversation: sanitizedMessages,
};
if (authPool) {
cachedGooseSession = await repairSessionConversationFromDb(
authPool,
cachedGooseSession,
sessionId,
req.currentUser.id,
);
}
return res.json(cachedGooseSession);
}
}
@@ -4494,13 +4506,21 @@ api.get('/sessions/:sessionId', async (req, res, next) => {
const text = await upstream.text().catch(() => '');
return res.status(upstream.status).send(text);
}
const gooseSession = await upstream.json();
let gooseSession = await upstream.json();
if (Array.isArray(gooseSession.conversation)) {
gooseSession.conversation = sanitizeSessionConversationPublicHtmlLinks(
gooseSession.conversation,
req.currentUser,
);
}
if (authPool) {
gooseSession = await repairSessionConversationFromDb(
authPool,
gooseSession,
sessionId,
req.currentUser.id,
);
}
// Write-through: persist snapshot async, don't block the response.
if (sessionSnapshotService?.isEnabled()) {
const messages = (gooseSession.conversation ?? [])