mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 18:35:17 +00:00
feat(timeline): preserve history across reload and archive
Reload agent used to delete the timeline and replay the provider's history under a new epoch. That brought back turns the user had rewound and dropped anything the provider no longer had. Archive deleted the timeline too. Paseo's timeline is now the owner of the conversation. Reload agent keeps it and reconciles the provider's history append-only: turns are aligned by provider prompt id and compared exactly (prompt text, assistant and reasoning text, tool calls). When the owned conversation is a whole-segment prefix of the provider's, the rows after it are appended as new live rows. Any other difference changes nothing and agent_refreshed reports historyDiverged, which the app shows as a warning on Reload agent and Unarchive. Divergence is deliberately conservative: - An answer or tool call Paseo saw only partly can never be completed by a reload. History rows carry no turn id to update a live row in place. - A prompt sent with attachments diverges on every reload. Providers store the rendered attachment text, and the owned row keeps only the typed text. Recording the sent prompt is left to provider handoff work. Archive now only closes the runtime. Stored history, including provider children, is read without resuming or even requiring the provider. Forced provider replacement is removed. Tradeoff: an archived chat's history stays in memory until the daemon exits, measured at about 1.1-1.2x its JSON size (about 0.5 MB for a typical session, about 16 MB for a heavy tool session). Moving retained history to disk belongs to the persisted backend, not eviction, which would drop the rewinds this change keeps.
This commit is contained in:
@@ -6,6 +6,7 @@ import { FOOTER_HEIGHT } from "@/constants/layout";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { KeyboardTranslateView } from "@/keyboard/shift";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import { toErrorMessage } from "@/utils/error-messages";
|
||||
|
||||
@@ -18,6 +19,7 @@ export function ArchivedAgentCallout({ serverId, agentId }: ArchivedAgentCallout
|
||||
const { t } = useTranslation();
|
||||
const client = useHostRuntimeClient(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
const toast = useToast();
|
||||
const [isUnarchiving, setIsUnarchiving] = useState(false);
|
||||
const [unarchiveError, setUnarchiveError] = useState<string | null>(null);
|
||||
|
||||
@@ -26,12 +28,15 @@ export function ArchivedAgentCallout({ serverId, agentId }: ArchivedAgentCallout
|
||||
setIsUnarchiving(true);
|
||||
setUnarchiveError(null);
|
||||
try {
|
||||
await client.refreshAgent(agentId);
|
||||
const refreshed = await client.refreshAgent(agentId);
|
||||
if (refreshed.historyDiverged) {
|
||||
toast.show(t("workspace.tabs.toasts.reloadedAgentHistoryDiverged"), { variant: "warning" });
|
||||
}
|
||||
} catch (error) {
|
||||
setUnarchiveError(toErrorMessage(error));
|
||||
setIsUnarchiving(false);
|
||||
}
|
||||
}, [client, isConnected, isUnarchiving, agentId]);
|
||||
}, [client, isConnected, isUnarchiving, agentId, toast, t]);
|
||||
|
||||
return (
|
||||
<KeyboardTranslateView style={styles.container}>
|
||||
|
||||
@@ -699,6 +699,8 @@ export const ar: TranslationResources = {
|
||||
resumeCommandUnavailable: "أمر الاستئناف غير متوفر",
|
||||
reloadingAgent: "وكيل إعادة التحميل...",
|
||||
reloadedAgent: "وكيل إعادة تحميل",
|
||||
reloadedAgentHistoryDiverged:
|
||||
"تمت إعادة تحميل الوكيل، لكن سجل المزوّد لا يطابق هذه المحادثة، لذا لم تتم مزامنته",
|
||||
failedToReloadAgent: "فشل في إعادة تحميل الوكيل",
|
||||
failedToCloseAgent: "فشل في إغلاق الوكيل",
|
||||
},
|
||||
|
||||
@@ -696,6 +696,8 @@ export const en = {
|
||||
resumeCommandUnavailable: "Resume command not available",
|
||||
reloadingAgent: "Reloading agent...",
|
||||
reloadedAgent: "Reloaded agent",
|
||||
reloadedAgentHistoryDiverged:
|
||||
"Reloaded agent, but the provider's history doesn't match this chat, so it wasn't synced",
|
||||
failedToReloadAgent: "Failed to reload agent",
|
||||
failedToCloseAgent: "Failed to close agent",
|
||||
},
|
||||
|
||||
@@ -706,6 +706,8 @@ export const es: TranslationResources = {
|
||||
resumeCommandUnavailable: "Comando de reanudación no disponible",
|
||||
reloadingAgent: "Agente de recarga...",
|
||||
reloadedAgent: "Agente recargado",
|
||||
reloadedAgentHistoryDiverged:
|
||||
"Agente recargado, pero el historial del proveedor no coincide con este chat, así que no se sincronizó",
|
||||
failedToReloadAgent: "No se pudo recargar el agente",
|
||||
failedToCloseAgent: "No se pudo cerrar el agente",
|
||||
},
|
||||
|
||||
@@ -706,6 +706,8 @@ export const fr: TranslationResources = {
|
||||
resumeCommandUnavailable: "Commande de reprise indisponible",
|
||||
reloadingAgent: "Rechargement de l’agent…",
|
||||
reloadedAgent: "Agent rechargé",
|
||||
reloadedAgentHistoryDiverged:
|
||||
"Agent rechargé, mais l’historique du fournisseur ne correspond pas à cette conversation, il n’a donc pas été synchronisé",
|
||||
failedToReloadAgent: "Impossible de recharger l’agent",
|
||||
failedToCloseAgent: "Impossible de fermer l’agent",
|
||||
},
|
||||
|
||||
@@ -706,6 +706,8 @@ export const ja: TranslationResources = {
|
||||
resumeCommandUnavailable: "再開コマンドが利用できません",
|
||||
reloadingAgent: "エージェントを再読み込み中...",
|
||||
reloadedAgent: "エージェントを再読み込みしました",
|
||||
reloadedAgentHistoryDiverged:
|
||||
"エージェントを再読み込みしましたが、プロバイダーの履歴がこのチャットと一致しないため同期しませんでした",
|
||||
failedToReloadAgent: "エージェントの再読み込みに失敗しました",
|
||||
failedToCloseAgent: "エージェントを閉じられませんでした",
|
||||
},
|
||||
|
||||
@@ -701,6 +701,8 @@ export const ko: TranslationResources = {
|
||||
resumeCommandUnavailable: "재개 명령을 사용할 수 없습니다",
|
||||
reloadingAgent: "에이전트 다시 로드 중...",
|
||||
reloadedAgent: "에이전트를 다시 로드했습니다",
|
||||
reloadedAgentHistoryDiverged:
|
||||
"에이전트를 다시 로드했지만 제공자 기록이 이 채팅과 일치하지 않아 동기화하지 않았습니다",
|
||||
failedToReloadAgent: "에이전트를 다시 로드하지 못했습니다",
|
||||
failedToCloseAgent: "에이전트를 닫지 못했습니다",
|
||||
},
|
||||
|
||||
@@ -704,6 +704,8 @@ export const ptBR: TranslationResources = {
|
||||
resumeCommandUnavailable: "Comando de retomada indisponível",
|
||||
reloadingAgent: "Recarregando agente...",
|
||||
reloadedAgent: "Agente recarregado",
|
||||
reloadedAgentHistoryDiverged:
|
||||
"Agente recarregado, mas o histórico do provedor não corresponde a este chat, então não foi sincronizado",
|
||||
failedToReloadAgent: "Falha ao recarregar agente",
|
||||
failedToCloseAgent: "Falha ao fechar agente",
|
||||
},
|
||||
|
||||
@@ -706,6 +706,8 @@ export const ru: TranslationResources = {
|
||||
resumeCommandUnavailable: "Команда возобновления недоступна",
|
||||
reloadingAgent: "Перезагрузка агента...",
|
||||
reloadedAgent: "Агент перезагружен",
|
||||
reloadedAgentHistoryDiverged:
|
||||
"Агент перезагружен, но история провайдера не совпадает с этим чатом, поэтому она не синхронизирована",
|
||||
failedToReloadAgent: "Не удалось перезагрузить агента",
|
||||
failedToCloseAgent: "Не удалось закрыть агента",
|
||||
},
|
||||
|
||||
@@ -699,6 +699,8 @@ export const zhCN: TranslationResources = {
|
||||
resumeCommandUnavailable: "恢复命令不可用",
|
||||
reloadingAgent: "正在重新加载 Agent...",
|
||||
reloadedAgent: "已重新加载 Agent",
|
||||
reloadedAgentHistoryDiverged:
|
||||
"已重新加载 Agent,但提供方的历史记录与此对话不一致,因此未同步",
|
||||
failedToReloadAgent: "重新加载 Agent 失败",
|
||||
failedToCloseAgent: "关闭 Agent 失败",
|
||||
},
|
||||
|
||||
@@ -2162,11 +2162,10 @@ function WorkspaceScreenContent({
|
||||
|
||||
toast.show(t("workspace.tabs.toasts.reloadingAgent"), { durationMs: null });
|
||||
try {
|
||||
await client.refreshAgent(agentId);
|
||||
// Send the existing cursor so the server detects the new epoch and
|
||||
// returns reset:true. Without a cursor, the server returns reset:false
|
||||
// and the client takes the incremental path, where new-epoch rows are
|
||||
// dropped against the stale cursor.
|
||||
const refreshed = await client.refreshAgent(agentId);
|
||||
// Send the existing cursor: Reload agent keeps the history, so the server
|
||||
// returns only the rows it added. When the history was imported again
|
||||
// under a new epoch, the cursor reads as stale and the server resets.
|
||||
const sessionState = useSessionStore.getState().sessions[normalizedServerId];
|
||||
const currentCursor = sessionState?.agentTimelineCursor.get(agentId);
|
||||
await getHostRuntimeStore().fetchAgentTimeline(normalizedServerId, agentId, {
|
||||
@@ -2175,7 +2174,13 @@ function WorkspaceScreenContent({
|
||||
? { cursor: { epoch: currentCursor.epoch, seq: currentCursor.endSeq } }
|
||||
: {}),
|
||||
});
|
||||
toast.show(t("workspace.tabs.toasts.reloadedAgent"), { variant: "success" });
|
||||
if (refreshed.historyDiverged) {
|
||||
toast.show(t("workspace.tabs.toasts.reloadedAgentHistoryDiverged"), {
|
||||
variant: "warning",
|
||||
});
|
||||
} else {
|
||||
toast.show(t("workspace.tabs.toasts.reloadedAgent"), { variant: "success" });
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : t("workspace.tabs.toasts.failedToReloadAgent"),
|
||||
|
||||
Reference in New Issue
Block a user