refactor: Unify reminder management across interactive and headless modes (#1001)

Co-authored-by: Letta <noreply@letta.com>
Co-authored-by: cpacker <packercharles@gmail.com>
This commit is contained in:
Devansh Jain
2026-02-18 15:44:59 -08:00
committed by GitHub
parent 5b12a30293
commit b322a46a43
13 changed files with 907 additions and 395 deletions

View File

@@ -82,6 +82,12 @@ import {
type RalphState,
ralphMode,
} from "../ralph/mode";
import { buildSharedReminderParts } from "../reminders/engine";
import {
createSharedReminderState,
resetSharedReminderState,
syncReminderStateFromContextTracker,
} from "../reminders/state";
import { updateProjectSettings } from "../settings";
import { settingsManager } from "../settings-manager";
import { telemetry } from "../telemetry";
@@ -191,13 +197,10 @@ import {
import { formatCompact } from "./helpers/format";
import { parsePatchOperations } from "./helpers/formatArgsDisplay";
import {
buildCompactionMemoryReminder,
buildMemoryReminder,
getReflectionSettings,
parseMemoryPreference,
type ReflectionSettings,
reflectionSettingsToLegacyMode,
shouldFireStepCountTrigger,
} from "./helpers/memoryReminder";
import {
type QueuedMessage,
@@ -1503,33 +1506,16 @@ export default function App({
// Show exit stats on exit (double Ctrl+C)
const [showExitStats, setShowExitStats] = useState(false);
// Track if we've sent the session context for this CLI session
const hasSentSessionContextRef = useRef(false);
const sharedReminderStateRef = useRef(createSharedReminderState());
// Track if we've set the conversation summary for this new conversation
// Initialized to true for resumed conversations (they already have context)
const hasSetConversationSummaryRef = useRef(resumedExistingConversation);
// Store first user query for conversation summary
const firstUserQueryRef = useRef<string | null>(null);
// Track skills injection state (LET-7353)
const discoveredSkillsRef = useRef<import("../agent/skills").Skill[] | null>(
null,
);
const hasInjectedSkillsRef = useRef(false);
const resetBootstrapReminderState = useCallback(() => {
hasSentSessionContextRef.current = false;
hasInjectedSkillsRef.current = false;
discoveredSkillsRef.current = null;
resetSharedReminderState(sharedReminderStateRef.current);
}, []);
// Track conversation turn count for periodic memory reminders
const turnCountRef = useRef(0);
// Track last notified permission mode to detect changes
const lastNotifiedModeRef = useRef<PermissionMode>("default");
// Static items (things that are done rendering and can be frozen)
const [staticItems, setStaticItems] = useState<StaticItem[]>([]);
@@ -2778,8 +2764,10 @@ export default function App({
// Git-backed memory: check status periodically (fire-and-forget).
// Runs every N turns to detect uncommitted changes or unpushed commits.
const isIntervalTurn =
turnCountRef.current > 0 &&
turnCountRef.current % MEMFS_CONFLICT_CHECK_INTERVAL === 0;
sharedReminderStateRef.current.turnCount > 0 &&
sharedReminderStateRef.current.turnCount %
MEMFS_CONFLICT_CHECK_INTERVAL ===
0;
if (isIntervalTurn && !memfsGitCheckInFlightRef.current) {
memfsGitCheckInFlightRef.current = true;
@@ -5150,9 +5138,6 @@ export default function App({
setStaticRenderEpoch((e) => e + 1);
resetTrajectoryBases();
// Reset turn counter for memory reminders when switching agents
turnCountRef.current = 0;
// Update agent state - also update ref immediately for any code that runs before re-render
agentIdRef.current = targetAgentId;
setAgentId(targetAgentId);
@@ -5291,9 +5276,6 @@ export default function App({
setStaticRenderEpoch((e) => e + 1);
resetTrajectoryBases();
// Reset turn counter for memory reminders
turnCountRef.current = 0;
// Update agent state
agentIdRef.current = agent.id;
setAgentId(agent.id);
@@ -6622,9 +6604,6 @@ export default function App({
// Ensure bootstrap reminders are re-injected for the new conversation.
resetBootstrapReminderState();
// Reset turn counter for memory reminders
turnCountRef.current = 0;
// Re-run SessionStart hooks for new conversation
sessionHooksRanRef.current = false;
runSessionStartHooks(
@@ -6703,9 +6682,6 @@ export default function App({
// Ensure bootstrap reminders are re-injected for the new conversation.
resetBootstrapReminderState();
// Reset turn counter for memory reminders
turnCountRef.current = 0;
// Re-run SessionStart hooks for new conversation
sessionHooksRanRef.current = false;
runSessionStartHooks(
@@ -8088,9 +8064,6 @@ ${SYSTEM_REMINDER_CLOSE}`;
const contentParts =
overrideContentParts ?? buildMessageContentFromDisplay(msg);
// Prepend plan mode reminder if in plan mode
const planModeReminder = getPlanModeReminder();
// Prepend ralph mode reminder if in ralph mode
let ralphModeReminder = "";
if (ralphMode.getState().isActive) {
@@ -8106,30 +8079,6 @@ ${SYSTEM_REMINDER_CLOSE}`;
}
}
// Prepend session context on first message of CLI session (if enabled)
let sessionContextReminder = "";
const sessionContextEnabled = settingsManager.getSetting(
"sessionContextEnabled",
);
if (
!hasSentSessionContextRef.current &&
sessionContextEnabled &&
sessionContextReminderEnabled
) {
const { buildSessionContext } = await import(
"./helpers/sessionContext"
);
sessionContextReminder = buildSessionContext({
agentInfo: {
id: agentId,
name: agentName,
description: agentDescription,
lastRunAt: agentLastRunAt,
},
});
hasSentSessionContextRef.current = true;
}
// Inject SessionStart hook feedback (stdout on exit 2) into first message only
let sessionStartHookFeedback = "";
if (sessionStartFeedbackRef.current.length > 0) {
@@ -8155,24 +8104,6 @@ ${SYSTEM_REMINDER_CLOSE}
const reflectionSettings = getReflectionSettings();
const memfsEnabledForAgent = settingsManager.isMemfsEnabled(agentId);
const shouldFireStepTrigger = shouldFireStepCountTrigger(
turnCountRef.current,
reflectionSettings,
);
let memoryReminderContent = "";
if (
shouldFireStepTrigger &&
(reflectionSettings.behavior === "reminder" || !memfsEnabledForAgent)
) {
// Step-count reminder mode (or non-memfs fallback)
memoryReminderContent = await buildMemoryReminder(
turnCountRef.current,
agentId,
);
}
// Increment turn count for next iteration
turnCountRef.current += 1;
// Build git memory sync reminder if uncommitted changes or unpushed commits
let memoryGitReminder = "";
@@ -8198,20 +8129,6 @@ ${SYSTEM_REMINDER_CLOSE}
pendingGitReminderRef.current = null;
}
// Build permission mode change alert if mode changed since last notification
let permissionModeAlert = "";
const currentMode = permissionMode.getMode();
if (currentMode !== lastNotifiedModeRef.current) {
const modeDescriptions: Record<PermissionMode, string> = {
default: "Normal approval flow.",
acceptEdits: "File edits auto-approved.",
plan: "Read-only mode. Focus on exploration and planning.",
bypassPermissions: "All tools auto-approved. Bias toward action.",
};
permissionModeAlert = `${SYSTEM_REMINDER_OPEN}Permission mode changed to: ${currentMode}. ${modeDescriptions[currentMode]}${SYSTEM_REMINDER_CLOSE}\n\n`;
lastNotifiedModeRef.current = currentMode;
}
// Combine reminders with content as separate text parts.
// This preserves each reminder boundary in the API payload.
// Note: Task notifications now come through messageQueue directly (added by messageQueueBridge)
@@ -8257,56 +8174,28 @@ ${SYSTEM_REMINDER_CLOSE}
return false;
}
};
pushReminder(sessionContextReminder);
// Inject available skills as system-reminder (LET-7353)
// Discover each turn so on-disk skill changes can trigger reinjection.
{
const {
discoverSkills: discover,
SKILLS_DIR: defaultDir,
formatSkillsAsSystemReminder,
} = await import("../agent/skills");
const { getSkillsDirectory, getSkillSources } = await import(
"../agent/context"
);
const previousSkillsReminder = discoveredSkillsRef.current
? formatSkillsAsSystemReminder(discoveredSkillsRef.current)
: null;
let latestSkills = discoveredSkillsRef.current ?? [];
try {
const skillsDir =
getSkillsDirectory() || join(process.cwd(), defaultDir);
const { skills } = await discover(skillsDir, agentId, {
sources: getSkillSources(),
});
latestSkills = skills;
} catch {
// Keep the previous snapshot when discovery fails.
}
discoveredSkillsRef.current = latestSkills;
const latestSkillsReminder = formatSkillsAsSystemReminder(
discoveredSkillsRef.current,
);
if (
previousSkillsReminder !== null &&
previousSkillsReminder !== latestSkillsReminder
) {
contextTrackerRef.current.pendingSkillsReinject = true;
}
const needsSkillsReinject =
contextTrackerRef.current.pendingSkillsReinject;
if (!hasInjectedSkillsRef.current || needsSkillsReinject) {
if (latestSkillsReminder) {
pushReminder(latestSkillsReminder);
}
hasInjectedSkillsRef.current = true;
contextTrackerRef.current.pendingSkillsReinject = false;
}
syncReminderStateFromContextTracker(
sharedReminderStateRef.current,
contextTrackerRef.current,
);
const { getSkillSources } = await import("../agent/context");
const { parts: sharedReminderParts } = await buildSharedReminderParts({
mode: "interactive",
agent: {
id: agentId,
name: agentName,
description: agentDescription,
lastRunAt: agentLastRunAt,
},
state: sharedReminderStateRef.current,
sessionContextReminderEnabled,
reflectionSettings,
skillSources: getSkillSources(),
resolvePlanModeReminder: getPlanModeReminder,
maybeLaunchReflectionSubagent,
});
for (const part of sharedReminderParts) {
reminderParts.push(part);
}
// Build conversation switch alert if a switch is pending (behind feature flag)
@@ -8325,41 +8214,10 @@ ${SYSTEM_REMINDER_CLOSE}
pendingConversationSwitchRef.current = null;
pushReminder(sessionStartHookFeedback);
pushReminder(permissionModeAlert);
pushReminder(conversationSwitchAlert);
pushReminder(planModeReminder);
pushReminder(ralphModeReminder);
pushReminder(bashCommandPrefix);
pushReminder(userPromptSubmitHookFeedback);
pushReminder(memoryReminderContent);
// Step-count auto-launch mode: fire reflection in background on interval.
if (
shouldFireStepTrigger &&
reflectionSettings.trigger === "step-count" &&
reflectionSettings.behavior === "auto-launch"
) {
await maybeLaunchReflectionSubagent("step-count");
}
// Consume compaction-triggered reflection behavior on next user turn.
if (contextTrackerRef.current.pendingReflectionTrigger) {
contextTrackerRef.current.pendingReflectionTrigger = false;
if (reflectionSettings.trigger === "compaction-event") {
if (
reflectionSettings.behavior === "auto-launch" &&
memfsEnabledForAgent
) {
await maybeLaunchReflectionSubagent("compaction-event");
} else {
const compactionReminderContent =
await buildCompactionMemoryReminder(agentId);
pushReminder(compactionReminderContent);
}
}
}
pushReminder(memoryGitReminder);
const messageContent =
reminderParts.length > 0