diff --git a/src/cli/App.tsx b/src/cli/App.tsx
index 30eb47b..e1d3dc6 100644
--- a/src/cli/App.tsx
+++ b/src/cli/App.tsx
@@ -865,6 +865,11 @@ export default function App({
const resumeKey = useSuspend();
+ // Pending conversation switch context — consumed on first message after a switch
+ const pendingConversationSwitchRef = useRef<
+ import("./helpers/conversationSwitchAlert").ConversationSwitchContext | null
+ >(null);
+
// Track previous prop values to detect actual prop changes (not internal state changes)
const prevInitialAgentIdRef = useRef(initialAgentId);
const prevInitialAgentStateRef = useRef(initialAgentState);
@@ -4997,6 +5002,31 @@ export default function App({
setLlmConfig(agent.llm_config);
setConversationId(targetConversationId);
+ // Set conversation switch context for agent switch
+ {
+ const { getModelDisplayName } = await import("../agent/model");
+ const modelHandle =
+ agent.model ||
+ (agent.llm_config?.model_endpoint_type && agent.llm_config?.model
+ ? `${agent.llm_config.model_endpoint_type}/${agent.llm_config.model}`
+ : null);
+ const modelLabel =
+ (modelHandle && getModelDisplayName(modelHandle)) ||
+ modelHandle ||
+ "unknown";
+ pendingConversationSwitchRef.current = {
+ origin: "agent-switch",
+ conversationId: targetConversationId,
+ isDefault: targetConversationId === "default",
+ agentSwitchContext: {
+ name: agent.name || targetAgentId,
+ description: agent.description ?? undefined,
+ model: modelLabel,
+ blockCount: agent.blocks?.length ?? 0,
+ },
+ };
+ }
+
// Reset context token tracking for new agent
resetContextHistory(contextTrackerRef.current);
@@ -6379,6 +6409,12 @@ export default function App({
// Update conversationId state
setConversationId(conversation.id);
+ pendingConversationSwitchRef.current = {
+ origin: "new",
+ conversationId: conversation.id,
+ isDefault: false,
+ };
+
// Save the new session to settings
settingsManager.setLocalLastSession(
{ agentId, conversationId: conversation.id },
@@ -6451,6 +6487,13 @@ export default function App({
isolated_block_labels: [...ISOLATED_BLOCK_LABELS],
});
setConversationId(conversation.id);
+
+ pendingConversationSwitchRef.current = {
+ origin: "clear",
+ conversationId: conversation.id,
+ isDefault: false,
+ };
+
settingsManager.setLocalLastSession(
{ agentId, conversationId: conversation.id },
process.cwd(),
@@ -6777,6 +6820,15 @@ export default function App({
// Only update state after validation succeeds
setConversationId(targetConvId);
+
+ pendingConversationSwitchRef.current = {
+ origin: "resume-direct",
+ conversationId: targetConvId,
+ isDefault: targetConvId === "default",
+ messageCount: resumeData.messageHistory.length,
+ messageHistory: resumeData.messageHistory,
+ };
+
settingsManager.setLocalLastSession(
{ agentId, conversationId: targetConvId },
process.cwd(),
@@ -8047,8 +8099,24 @@ ${SYSTEM_REMINDER_CLOSE}
}
}
+ // Build conversation switch alert if a switch is pending (behind feature flag)
+ let conversationSwitchAlert = "";
+ if (
+ pendingConversationSwitchRef.current &&
+ settingsManager.getSetting("conversationSwitchAlertEnabled")
+ ) {
+ const { buildConversationSwitchAlert } = await import(
+ "./helpers/conversationSwitchAlert"
+ );
+ conversationSwitchAlert = buildConversationSwitchAlert(
+ pendingConversationSwitchRef.current,
+ );
+ }
+ pendingConversationSwitchRef.current = null;
+
pushReminder(sessionStartHookFeedback);
pushReminder(permissionModeAlert);
+ pushReminder(conversationSwitchAlert);
pushReminder(planModeReminder);
pushReminder(ralphModeReminder);
@@ -9793,6 +9861,15 @@ ${SYSTEM_REMINDER_CLOSE}
);
setConversationId(action.conversationId);
+
+ pendingConversationSwitchRef.current = {
+ origin: "resume-selector",
+ conversationId: action.conversationId,
+ isDefault: action.conversationId === "default",
+ messageCount: resumeData.messageHistory.length,
+ messageHistory: resumeData.messageHistory,
+ };
+
settingsManager.setLocalLastSession(
{ agentId, conversationId: action.conversationId },
process.cwd(),
@@ -11009,7 +11086,7 @@ Plan file path: ${planFilePath}`;
agentId={agentId}
agentName={agentName ?? undefined}
currentConversationId={conversationId}
- onSelect={async (convId) => {
+ onSelect={async (convId, selectorContext) => {
const overlayCommand = consumeOverlayCommand("conversations");
closeOverlay();
@@ -11071,6 +11148,18 @@ Plan file path: ${planFilePath}`;
// Only update state after validation succeeds
setConversationId(convId);
+
+ pendingConversationSwitchRef.current = {
+ origin: "resume-selector",
+ conversationId: convId,
+ isDefault: convId === "default",
+ messageCount:
+ selectorContext?.messageCount ??
+ resumeData.messageHistory.length,
+ summary: selectorContext?.summary,
+ messageHistory: resumeData.messageHistory,
+ };
+
settingsManager.setLocalLastSession(
{ agentId, conversationId: convId },
process.cwd(),
@@ -11289,7 +11378,11 @@ Plan file path: ${planFilePath}`;
initialQuery={searchQuery || undefined}
agentId={agentId}
conversationId={conversationId}
- onOpenConversation={async (targetAgentId, targetConvId) => {
+ onOpenConversation={async (
+ targetAgentId,
+ targetConvId,
+ searchContext,
+ ) => {
const overlayCommand = consumeOverlayCommand("search");
closeOverlay();
@@ -11358,6 +11451,17 @@ Plan file path: ${planFilePath}`;
);
setConversationId(actualTargetConv);
+
+ pendingConversationSwitchRef.current = {
+ origin: "search",
+ conversationId: actualTargetConv,
+ isDefault: actualTargetConv === "default",
+ messageCount: resumeData.messageHistory.length,
+ messageHistory: resumeData.messageHistory,
+ searchQuery: searchContext?.query,
+ searchMessage: searchContext?.message,
+ };
+
settingsManager.setLocalLastSession(
{ agentId, conversationId: actualTargetConv },
process.cwd(),
diff --git a/src/cli/components/ConversationSelector.tsx b/src/cli/components/ConversationSelector.tsx
index 957055f..fe5c805 100644
--- a/src/cli/components/ConversationSelector.tsx
+++ b/src/cli/components/ConversationSelector.tsx
@@ -17,7 +17,13 @@ interface ConversationSelectorProps {
agentId: string;
agentName?: string;
currentConversationId: string;
- onSelect: (conversationId: string) => void;
+ onSelect: (
+ conversationId: string,
+ context?: {
+ summary?: string;
+ messageCount: number;
+ },
+ ) => void;
onNewConversation: () => void;
onCancel: () => void;
}
@@ -370,7 +376,10 @@ export function ConversationSelector({
} else if (key.return) {
const selected = pageConversations[selectedIndex];
if (selected?.conversation.id) {
- onSelect(selected.conversation.id);
+ onSelect(selected.conversation.id, {
+ summary: selected.conversation.summary ?? undefined,
+ messageCount: selected.messageCount,
+ });
}
} else if (key.escape) {
onCancel();
diff --git a/src/cli/components/MessageSearch.tsx b/src/cli/components/MessageSearch.tsx
index cc1b828..630c385 100644
--- a/src/cli/components/MessageSearch.tsx
+++ b/src/cli/components/MessageSearch.tsx
@@ -18,7 +18,11 @@ interface MessageSearchProps {
/** Current conversation ID for "current conv" filter */
conversationId?: string;
/** Callback when user wants to open a conversation */
- onOpenConversation?: (agentId: string, conversationId?: string) => void;
+ onOpenConversation?: (
+ agentId: string,
+ conversationId?: string,
+ searchContext?: { query: string; message: string },
+ ) => void;
}
const VISIBLE_ITEMS = 5;
@@ -347,7 +351,11 @@ export function MessageSearch({
conversation_id?: string;
};
if (msgData.agent_id) {
- onOpenConversation(msgData.agent_id, msgData.conversation_id);
+ const fullText = getMessageText(expandedMessage);
+ onOpenConversation(msgData.agent_id, msgData.conversation_id, {
+ query: activeQuery,
+ message: fullText,
+ });
}
}
return;
diff --git a/src/cli/helpers/conversationSwitchAlert.ts b/src/cli/helpers/conversationSwitchAlert.ts
new file mode 100644
index 0000000..8a18fe6
--- /dev/null
+++ b/src/cli/helpers/conversationSwitchAlert.ts
@@ -0,0 +1,154 @@
+import type { Message } from "@letta-ai/letta-client/resources/agents/messages";
+
+const SYSTEM_ALERT_OPEN = "";
+const SYSTEM_ALERT_CLOSE = "";
+
+const MAX_HISTORY_MESSAGES = 8;
+const MAX_MESSAGE_CHARS = 500;
+
+export interface ConversationSwitchContext {
+ origin:
+ | "resume-direct"
+ | "resume-selector"
+ | "new"
+ | "clear"
+ | "search"
+ | "agent-switch";
+ conversationId: string;
+ isDefault: boolean;
+
+ summary?: string;
+ messageCount?: number;
+ messageHistory?: Message[];
+
+ searchQuery?: string;
+ searchMessage?: string;
+
+ agentSwitchContext?: {
+ name: string;
+ description?: string;
+ model: string;
+ blockCount: number;
+ };
+}
+
+export function buildConversationSwitchAlert(
+ ctx: ConversationSwitchContext,
+): string {
+ const parts: string[] = [];
+
+ if (ctx.origin === "new" || ctx.origin === "clear") {
+ parts.push(
+ "New conversation started. This is a fresh conversation thread with no prior messages.",
+ );
+ parts.push(`Conversation: ${ctx.conversationId}`);
+ } else if (ctx.origin === "search") {
+ parts.push(
+ `Conversation switched. The user searched for "${ctx.searchQuery}" and jumped to this conversation based on a matching message.`,
+ );
+ if (ctx.searchMessage) {
+ parts.push(`Selected message: "${ctx.searchMessage}"`);
+ }
+ pushConversationMeta(parts, ctx);
+ pushMessageHistory(parts, ctx);
+ } else if (ctx.origin === "agent-switch" && ctx.agentSwitchContext) {
+ const a = ctx.agentSwitchContext;
+ parts.push("Switched to a different agent.");
+ parts.push(`Agent: ${a.name}`);
+ if (a.description) {
+ parts.push(`Description: ${a.description}`);
+ }
+ parts.push(
+ `Model: ${a.model} · ${a.blockCount} memory block${a.blockCount === 1 ? "" : "s"}`,
+ );
+ pushMessageHistory(parts, ctx);
+ parts.push(
+ "The conversation context has changed entirely — review the in-context messages.",
+ );
+ } else if (ctx.isDefault) {
+ parts.push(
+ "Switched to the agent's default conversation (the primary, non-isolated message history).",
+ );
+ parts.push(
+ "This conversation is shared across all sessions that don't use explicit conversation IDs.",
+ );
+ pushMessageHistory(parts, ctx);
+ parts.push("Review the in-context messages for full conversation history.");
+ } else {
+ const via =
+ ctx.origin === "resume-selector" ? "/resume selector" : "/resume";
+ parts.push(`Conversation resumed via ${via}.`);
+ pushConversationMeta(parts, ctx);
+ pushMessageHistory(parts, ctx);
+ parts.push("Review the in-context messages for full conversation history.");
+ }
+
+ return `${SYSTEM_ALERT_OPEN}\n${parts.join("\n")}\n${SYSTEM_ALERT_CLOSE}\n\n`;
+}
+
+function pushConversationMeta(
+ parts: string[],
+ ctx: ConversationSwitchContext,
+): void {
+ const label = ctx.isDefault ? "default" : ctx.conversationId;
+ const countSuffix =
+ ctx.messageCount != null ? ` (${ctx.messageCount} messages)` : "";
+ parts.push(`Conversation: ${label}${countSuffix}`);
+ if (ctx.summary) {
+ parts.push(`Summary: ${ctx.summary}`);
+ }
+}
+
+function extractMessageText(msg: Message): string | null {
+ const content = (
+ msg as Message & {
+ content?: string | Array<{ type?: string; text?: string }>;
+ }
+ ).content;
+
+ if (!content) return null;
+
+ if (typeof content === "string") return content.trim();
+
+ if (Array.isArray(content)) {
+ const texts = content
+ .filter(
+ (p): p is { type: string; text: string } =>
+ p?.type === "text" && !!p.text,
+ )
+ .map((p) => p.text.trim())
+ .filter(Boolean);
+ return texts.join("\n") || null;
+ }
+
+ return null;
+}
+
+function pushMessageHistory(
+ parts: string[],
+ ctx: ConversationSwitchContext,
+): void {
+ if (!ctx.messageHistory || ctx.messageHistory.length === 0) return;
+
+ const relevant = ctx.messageHistory
+ .filter(
+ (m) =>
+ m.message_type === "user_message" ||
+ m.message_type === "assistant_message",
+ )
+ .slice(-MAX_HISTORY_MESSAGES);
+
+ if (relevant.length === 0) return;
+
+ parts.push("Recent conversation messages:");
+ for (const msg of relevant) {
+ const text = extractMessageText(msg);
+ if (!text) continue;
+ const role = msg.message_type === "user_message" ? "user" : "assistant";
+ const clipped =
+ text.length > MAX_MESSAGE_CHARS
+ ? `${text.slice(0, MAX_MESSAGE_CHARS)}...`
+ : text;
+ parts.push(`[${role}] ${clipped}`);
+ }
+}
diff --git a/src/settings-manager.ts b/src/settings-manager.ts
index 23b9bcd..df464e5 100644
--- a/src/settings-manager.ts
+++ b/src/settings-manager.ts
@@ -59,6 +59,7 @@ export interface Settings {
reflectionTrigger: "off" | "step-count" | "compaction-event";
reflectionBehavior: "reminder" | "auto-launch";
reflectionStepCount: number;
+ conversationSwitchAlertEnabled: boolean; // Send system-alert when switching conversations/agents
globalSharedBlockIds: Record; // DEPRECATED: kept for backwards compat
profiles?: Record; // DEPRECATED: old format, kept for migration
pinnedAgents?: string[]; // DEPRECATED: kept for backwards compat, use pinnedAgentsByServer
@@ -116,6 +117,7 @@ const DEFAULT_SETTINGS: Settings = {
tokenStreaming: false,
showCompactions: false,
enableSleeptime: false,
+ conversationSwitchAlertEnabled: false,
sessionContextEnabled: true,
memoryReminderInterval: 25, // DEPRECATED: use reflection* fields
reflectionTrigger: "step-count",