fix(pending-messages): run compaction in background, return 202 Accepted

LLM summarization (30-120s) was blocking HTTP response causing browser
timeouts. Now runs in goroutine and returns immediately. UI polls every
5s until completion.
This commit is contained in:
viettranx committed 2026-03-10 20:33:16 +07:00
1 parent 06ac35eeb1
commit bec670ead0
6 files changed
+45 -23

No files matched your search

+15 -12
View File
@@ -151,22 +151,25 @@ func (h *PendingMessagesHandler) handleCompact(w http.ResponseWriter, r *http.Re
return
}
// Detach from HTTP request context so LLM summarization isn't
// cancelled when the browser closes the connection.
ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), 180*time.Second)
defer cancel()
// Run compaction in background so the HTTP response returns immediately.
// The long-running LLM call (30-120s) was blocking the response, which
// caused browser WebSocket connections to drop (pong timeout).
keepRecent := h.keepRecent
if keepRecent <= 0 {
keepRecent = 15
}
remaining, err := channels.CompactGroup(ctx, h.store, req.ChannelName, req.HistoryKey, provider, model, keepRecent, h.maxTokens)
if err != nil {
slog.Warn("compact.failed", "channel", req.ChannelName, "key", req.HistoryKey, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"status": "ok", "method": "summarized", "remaining": remaining})
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
defer cancel()
remaining, err := channels.CompactGroup(ctx, h.store, req.ChannelName, req.HistoryKey, provider, model, keepRecent, h.maxTokens)
if err != nil {
slog.Warn("compact.failed", "channel", req.ChannelName, "key", req.HistoryKey, "error", err)
} else {
slog.Info("compact.done", "channel", req.ChannelName, "key", req.HistoryKey, "remaining", remaining)
}
}()
writeJSON(w, http.StatusAccepted, map[string]interface{}{"status": "accepted", "method": "summarizing"})
}
// resolveProviderAndModel resolves the LLM provider+model for pending message compaction.
@@ -41,6 +41,8 @@
"compacted": "Compaction complete",
"compactedSummarized": "Summarized to {{count}} messages",
"compactedDeleted": "All messages deleted (no LLM provider available)",
"compacting": "Compacting…",
"compactingDesc": "LLM summarization running in background. List will refresh automatically.",
"failedCompact": "Compaction failed",
"cleared": "Messages cleared",
"failedClear": "Failed to clear messages"
@@ -41,6 +41,8 @@
"compacted": "Tóm tắt hoàn tất",
"compactedSummarized": "Đã tóm tắt còn {{count}} tin nhắn",
"compactedDeleted": "Đã xóa tất cả (không có LLM provider)",
"compacting": "Đang tóm tắt…",
"compactingDesc": "LLM đang tóm tắt ở nền. Danh sách sẽ tự động cập nhật.",
"failedCompact": "Tóm tắt thất bại",
"cleared": "Đã xóa tin nhắn",
"failedClear": "Không thể xóa tin nhắn"
@@ -41,6 +41,8 @@
"compacted": "压缩完成",
"compactedSummarized": "已压缩为 {{count}} 条消息",
"compactedDeleted": "已删除全部(无可用LLM Provider)",
"compacting": "压缩中…",
"compactingDesc": "LLM正在后台进行摘要。列表将自动刷新。",
"failedCompact": "压缩失败",
"cleared": "消息已清除",
"failedClear": "清除消息失败"
@@ -48,13 +48,19 @@ export function usePendingMessages() {
"/v1/pending-messages/compact",
{ channel_name: channel, history_key: key },
);
const method = res?.method ?? "summarized";
toast.success(
i18next.t("pending-messages:toast.compacted"),
method === "deleted"
? i18next.t("pending-messages:toast.compactedDeleted")
: i18next.t("pending-messages:toast.compactedSummarized", { count: res?.remaining ?? 0 }),
);
const method = res?.method ?? "summarizing";
if (method === "deleted") {
toast.success(
i18next.t("pending-messages:toast.compacted"),
i18next.t("pending-messages:toast.compactedDeleted"),
);
} else {
// Backend runs LLM in background — show info toast and let caller poll
toast.info(
i18next.t("pending-messages:toast.compacting"),
i18next.t("pending-messages:toast.compactingDesc"),
);
}
return true;
} catch (err) {
toast.error(
@@ -45,10 +45,17 @@ export function PendingMessagesPage() {
const key = `${group.channel_name}/${group.history_key}`;
setActionLoading(key);
setCompactingKey(key);
await compactGroup(group.channel_name, group.history_key);
setCompactingKey(null);
setActionLoading(null);
loadGroups();
const ok = await compactGroup(group.channel_name, group.history_key);
if (!ok) {
setCompactingKey(null);
setActionLoading(null);
return;
}
// Backend runs LLM in background — poll until done (has_summary changes).
// Compact button is already disabled when has_summary=true.
const poll = setInterval(() => loadGroups(), 5000);
setTimeout(() => loadGroups(), 2000); // quick first check
setTimeout(() => { clearInterval(poll); setCompactingKey(null); setActionLoading(null); }, 120_000);
};
const handleClear = async (group: PendingMessageGroup) => {