personality: Lock system prompt at session start

Amolith created

The old code injected a system-reminder custom message every turn after
the first, creating a persisted entry in the session file each time.
Long conversations accumulated dozens of personality blobs, wasting
tokens and cluttering session history.

Lock the personality into the system prompt at session start via the
ephemeral systemPrompt return from before_agent_start (never persisted,
same content each turn so provider cache stays valid). Mid-session
switches queue a single one-shot message instead of touching the system
prompt — once a conversation exists, its system prompt is immutable.

Change summary

packages/personality/src/index.ts | 108 +++++++++++++++++---------------
1 file changed, 57 insertions(+), 51 deletions(-)

Detailed changes

packages/personality/src/index.ts 🔗

@@ -91,75 +91,67 @@ function wrapPersonality(content: string): string {
 }
 
 export default function (pi: ExtensionAPI) {
+	// "active" tracks what the user last picked (persisted to disk).
+	// "session" is locked at session_start and never changes — it
+	// controls what goes into the system prompt for this session.
 	let activePersonality: string | null = null;
-	let personalityContent: string | null = null;
-	let isFirstTurn = true;
-
-	/** Load personality by name. Returns true if found and loaded. */
-	function loadPersonality(name: string): boolean {
-		const content = readPersonalityContent(name);
-		if (!content) return false;
+	let sessionPersonalityContent: string | null = null;
+	let pendingReminder: string | null = null;
 
+	/** Set the active personality name. Does NOT touch session prompt. */
+	function setActive(name: string | null): void {
 		activePersonality = name;
-		personalityContent = content;
-		return true;
-	}
-
-	/** Clear active personality (both in-memory and on disk). */
-	function clearPersonality(): void {
-		activePersonality = null;
-		personalityContent = null;
-		writeActivePersonality(null);
-	}
-
-	/** Switch to a personality: load into memory and persist to disk. */
-	function switchPersonality(name: string): boolean {
-		if (!loadPersonality(name)) return false;
 		writeActivePersonality(name);
-		return true;
 	}
 
-	// Restore persisted personality on session start
+	// Lock personality into the system prompt at session start.
+	// This content never changes for the lifetime of the session.
 	pi.on("session_start", async (_event, ctx) => {
 		const persisted = readActivePersonality();
-
-		// Determine whether this is a fresh session or a resumed one
-		const entries = ctx.sessionManager.getEntries();
-		const hasMessages = entries.some((e) => e.type === "message" && e.message.role === "assistant");
-		isFirstTurn = !hasMessages;
+		pendingReminder = null;
+		sessionPersonalityContent = null;
 
 		if (persisted) {
-			if (loadPersonality(persisted)) {
+			const content = readPersonalityContent(persisted);
+			if (content) {
+				activePersonality = persisted;
+				sessionPersonalityContent = content;
 				ctx.ui.notify(`Set personality to "${persisted}"`, "info");
 			} else {
 				// Personality file was removed since last session
-				clearPersonality();
+				setActive(null);
 			}
 		}
 	});
 
-	// Inject personality into the system prompt or as a reminder message.
-	// On the first turn (no prior messages), we modify the system prompt
-	// directly so the personality is baked into the initial cache.
-	// On subsequent turns, we inject a reminder message instead to avoid
-	// invalidating the inference cache.
+	// Append the session personality to the system prompt (ephemeral,
+	// never persisted). The content is identical every turn so the
+	// provider cache stays valid.
+	//
+	// Mid-session switches only fire a one-shot message — they never
+	// alter the system prompt.
 	pi.on("before_agent_start", async (event) => {
-		if (!personalityContent) return undefined;
+		if (!sessionPersonalityContent && !pendingReminder) return undefined;
 
-		if (isFirstTurn) {
-			isFirstTurn = false;
-			return {
-				systemPrompt: event.systemPrompt + wrapPersonality(personalityContent),
-			};
+		const result: {
+			systemPrompt?: string;
+			message?: { customType: string; content: string; display: boolean };
+		} = {};
+
+		if (sessionPersonalityContent) {
+			result.systemPrompt = event.systemPrompt + wrapPersonality(sessionPersonalityContent);
 		}
 
-		return {
-			message: {
-				customType: "system-reminder",
-				content: wrapPersonality(personalityContent),
+		if (pendingReminder) {
+			result.message = {
+				customType: "personality-switch",
+				content: pendingReminder,
 				display: false,
-			},
-		};
+			};
+			pendingReminder = null;
+		}
+
+		return result;
 	});
 
 	// Register /personality command
@@ -180,7 +172,8 @@ export default function (pi: ExtensionAPI) {
 					return;
 				}
 				const was = activePersonality;
-				clearPersonality();
+				pendingReminder = `Personality "${was}" has been cleared. Revert to your default behaviour.`;
+				setActive(null);
 				ctx.ui.notify(`Personality cleared (was: ${was})`, "info");
 				return;
 			}
@@ -192,7 +185,13 @@ export default function (pi: ExtensionAPI) {
 					ctx.ui.notify(`Personality "${arg}" not found`, "error");
 					return;
 				}
-				switchPersonality(arg);
+				const content = readPersonalityContent(arg);
+				if (!content) {
+					ctx.ui.notify(`Personality "${arg}" could not be read`, "error");
+					return;
+				}
+				pendingReminder = wrapPersonality(content);
+				setActive(arg);
 				ctx.ui.notify(`Set personality to "${arg}"`, "info");
 				return;
 			}
@@ -220,13 +219,20 @@ export default function (pi: ExtensionAPI) {
 					ctx.ui.notify("No personality is active", "info");
 				} else {
 					const was = activePersonality;
-					clearPersonality();
+					pendingReminder = `Personality "${was}" has been cleared. Revert to your default behaviour.`;
+					setActive(null);
 					ctx.ui.notify(`Personality cleared (was: ${was})`, "info");
 				}
 				return;
 			}
 
-			switchPersonality(picked);
+			const content = readPersonalityContent(picked);
+			if (!content) {
+				ctx.ui.notify(`Personality "${picked}" could not be read`, "error");
+				return;
+			}
+			pendingReminder = wrapPersonality(content);
+			setActive(picked);
 			ctx.ui.notify(`Set personality to "${picked}"`, "info");
 		},
 	});