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:
210
src/cli/App.tsx
210
src/cli/App.tsx
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user