feat: add client-side sleeptime settings + compaction reflection triggers (#923)

This commit is contained in:
Charles Packer
2026-02-11 19:18:22 -08:00
committed by GitHub
parent c3a7f6c646
commit 9ea10bf2ff
12 changed files with 975 additions and 41 deletions

View File

@@ -130,6 +130,7 @@ import { PinDialog, validateAgentName } from "./components/PinDialog";
import { ProviderSelector } from "./components/ProviderSelector";
import { ReasoningMessage } from "./components/ReasoningMessageRich";
import { formatDuration, formatUsageStats } from "./components/SessionStats";
import { SleeptimeSelector } from "./components/SleeptimeSelector";
// InlinePlanApproval kept for easy rollback if needed
// import { InlinePlanApproval } from "./components/InlinePlanApproval";
import { StatusMessage } from "./components/StatusMessage";
@@ -177,8 +178,12 @@ import {
import { formatCompact } from "./helpers/format";
import { parsePatchOperations } from "./helpers/formatArgsDisplay";
import {
buildCompactionMemoryReminder,
buildMemoryReminder,
getReflectionSettings,
parseMemoryPreference,
type ReflectionSettings,
reflectionSettingsToLegacyMode,
} from "./helpers/memoryReminder";
import {
type QueuedMessage,
@@ -310,6 +315,7 @@ const INTERACTIVE_SLASH_COMMANDS = new Set([
"/system",
"/subagents",
"/memory",
"/sleeptime",
"/mcp",
"/help",
"/agents",
@@ -712,6 +718,18 @@ function stripSystemReminders(text: string): string {
.trim();
}
function formatReflectionSettings(settings: ReflectionSettings): string {
if (settings.trigger === "off") {
return "Off";
}
const behaviorLabel =
settings.behavior === "auto-launch" ? "auto-launch" : "reminder";
if (settings.trigger === "compaction-event") {
return `Compaction event (${behaviorLabel})`;
}
return `Step count (every ${settings.stepCount} turns, ${behaviorLabel})`;
}
function buildTextParts(
...parts: Array<string | undefined | null>
): Array<{ type: "text"; text: string }> {
@@ -1122,6 +1140,7 @@ export default function App({
// Overlay/selector state - only one can be open at a time
type ActiveOverlay =
| "model"
| "sleeptime"
| "toolset"
| "system"
| "agent"
@@ -1180,6 +1199,11 @@ export default function App({
type QueuedOverlayAction =
| { type: "switch_agent"; agentId: string; commandId?: string }
| { type: "switch_model"; modelId: string; commandId?: string }
| {
type: "set_sleeptime";
settings: ReflectionSettings;
commandId?: string;
}
| {
type: "switch_conversation";
conversationId: string;
@@ -5566,6 +5590,18 @@ export default function App({
return { submitted: true };
}
// Special handling for /sleeptime command - opens reflection settings
if (trimmed === "/sleeptime") {
startOverlayCommand(
"sleeptime",
"/sleeptime",
"Opening sleeptime settings...",
"Sleeptime settings dismissed",
);
setActiveOverlay("sleeptime");
return { submitted: true };
}
// Special handling for /toolset command - opens selector
if (trimmed === "/toolset") {
startOverlayCommand(
@@ -6430,6 +6466,11 @@ export default function App({
// Update command with success
cmd.finish(outputLines.join("\n"), true);
// Manual /compact bypasses stream compaction events, so trigger
// post-compaction reminder/skills reinjection on the next user turn.
contextTrackerRef.current.pendingReflectionTrigger = true;
contextTrackerRef.current.pendingSkillsReinject = true;
} catch (error) {
let errorOutput: string;
@@ -7760,6 +7801,7 @@ ${SYSTEM_REMINDER_CLOSE}
turnCountRef.current,
agentId,
);
const reflectionSettings = getReflectionSettings();
// Increment turn count for next iteration
turnCountRef.current += 1;
@@ -7854,6 +7896,17 @@ ${SYSTEM_REMINDER_CLOSE}
pushReminder(bashCommandPrefix);
pushReminder(userPromptSubmitHookFeedback);
pushReminder(memoryReminderContent);
// Consume compaction-triggered reflection/check reminder on next user turn.
if (contextTrackerRef.current.pendingReflectionTrigger) {
contextTrackerRef.current.pendingReflectionTrigger = false;
if (reflectionSettings.trigger === "compaction-event") {
const compactionReminderContent =
await buildCompactionMemoryReminder(agentId);
pushReminder(compactionReminderContent);
}
}
pushReminder(memoryGitReminder);
const messageContent =
reminderParts.length > 0
@@ -9373,6 +9426,79 @@ ${SYSTEM_REMINDER_CLOSE}
],
);
const handleSleeptimeModeSelect = useCallback(
async (
reflectionSettings: ReflectionSettings,
commandId?: string | null,
) => {
const overlayCommand = commandId
? commandRunner.getHandle(commandId, "/sleeptime")
: consumeOverlayCommand("sleeptime");
if (isAgentBusy()) {
setActiveOverlay(null);
const cmd =
overlayCommand ??
commandRunner.start(
"/sleeptime",
"Sleeptime settings update queued will apply after current task completes",
);
cmd.update({
output:
"Sleeptime settings update queued will apply after current task completes",
phase: "running",
});
setQueuedOverlayAction({
type: "set_sleeptime",
settings: reflectionSettings,
commandId: cmd.id,
});
return;
}
await withCommandLock(async () => {
const cmd =
overlayCommand ??
commandRunner.start("/sleeptime", "Saving sleeptime settings...");
cmd.update({
output: "Saving sleeptime settings...",
phase: "running",
});
try {
const legacyMode = reflectionSettingsToLegacyMode(reflectionSettings);
settingsManager.updateLocalProjectSettings({
memoryReminderInterval: legacyMode,
reflectionTrigger: reflectionSettings.trigger,
reflectionBehavior: reflectionSettings.behavior,
reflectionStepCount: reflectionSettings.stepCount,
});
settingsManager.updateSettings({
memoryReminderInterval: legacyMode,
reflectionTrigger: reflectionSettings.trigger,
reflectionBehavior: reflectionSettings.behavior,
reflectionStepCount: reflectionSettings.stepCount,
});
cmd.finish(
`Updated sleeptime settings to: ${formatReflectionSettings(reflectionSettings)}`,
true,
);
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
cmd.fail(`Failed to save sleeptime settings: ${errorDetails}`);
}
});
},
[
agentId,
commandRunner,
consumeOverlayCommand,
isAgentBusy,
withCommandLock,
],
);
const handleToolsetSelect = useCallback(
async (
toolsetId:
@@ -9462,6 +9588,8 @@ ${SYSTEM_REMINDER_CLOSE}
} else if (action.type === "switch_model") {
// Call handleModelSelect - it will see isAgentBusy() as false now
handleModelSelect(action.modelId, action.commandId);
} else if (action.type === "set_sleeptime") {
handleSleeptimeModeSelect(action.settings, action.commandId);
} else if (action.type === "switch_conversation") {
const cmd = action.commandId
? commandRunner.getHandle(action.commandId, "/resume")
@@ -9531,6 +9659,7 @@ ${SYSTEM_REMINDER_CLOSE}
queuedOverlayAction,
handleAgentSelect,
handleModelSelect,
handleSleeptimeModeSelect,
handleToolsetSelect,
handleSystemPromptSelect,
agentId,
@@ -10603,6 +10732,15 @@ Plan file path: ${planFilePath}`;
/>
)}
{activeOverlay === "sleeptime" && (
<SleeptimeSelector
initialSettings={getReflectionSettings()}
memfsEnabled={settingsManager.isMemfsEnabled(agentId)}
onSave={handleSleeptimeModeSelect}
onCancel={closeOverlay}
/>
)}
{/* Provider Selector - for connecting BYOK providers */}
{activeOverlay === "connect" && (
<ProviderSelector

View File

@@ -60,6 +60,14 @@ export const commands: Record<string, Command> = {
return "Opening memory viewer...";
},
},
"/sleeptime": {
desc: "Configure reflection reminder trigger settings",
order: 15.5,
handler: () => {
// Handled specially in App.tsx to open sleeptime settings
return "Opening sleeptime settings...";
},
},
"/memfs": {
desc: "Manage filesystem-backed memory (/memfs [enable|disable|sync|reset])",
args: "[enable|disable|sync|reset]",

View File

@@ -0,0 +1,372 @@
import { Box, useInput } from "ink";
import { useEffect, useMemo, useState } from "react";
import type {
ReflectionBehavior,
ReflectionSettings,
ReflectionTrigger,
} from "../helpers/memoryReminder";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
import { Text } from "./Text";
const SOLID_LINE = "─";
const DEFAULT_STEP_COUNT = "25";
const BEHAVIOR_OPTIONS: ReflectionBehavior[] = ["reminder", "auto-launch"];
type FocusRow = "trigger" | "behavior" | "step-count";
interface SleeptimeSelectorProps {
initialSettings: ReflectionSettings;
memfsEnabled: boolean;
onSave: (settings: ReflectionSettings) => void;
onCancel: () => void;
}
function getTriggerOptions(memfsEnabled: boolean): ReflectionTrigger[] {
return memfsEnabled
? ["off", "step-count", "compaction-event"]
: ["off", "step-count"];
}
function cycleOption<T extends string>(
options: readonly T[],
current: T,
direction: -1 | 1,
): T {
if (options.length === 0) {
return current;
}
const currentIndex = options.indexOf(current);
const safeIndex = currentIndex >= 0 ? currentIndex : 0;
const nextIndex = (safeIndex + direction + options.length) % options.length;
return options[nextIndex] ?? current;
}
function parseInitialState(initialSettings: ReflectionSettings): {
trigger: ReflectionTrigger;
behavior: ReflectionBehavior;
stepCount: string;
} {
return {
trigger:
initialSettings.trigger === "off" ||
initialSettings.trigger === "step-count" ||
initialSettings.trigger === "compaction-event"
? initialSettings.trigger
: "step-count",
behavior:
initialSettings.behavior === "auto-launch" ? "auto-launch" : "reminder",
stepCount: String(
Number.isInteger(initialSettings.stepCount) &&
initialSettings.stepCount > 0
? initialSettings.stepCount
: Number(DEFAULT_STEP_COUNT),
),
};
}
function parseStepCount(raw: string): number | null {
const trimmed = raw.trim();
if (!/^\d+$/.test(trimmed)) return null;
const parsed = Number.parseInt(trimmed, 10);
if (!Number.isInteger(parsed) || parsed <= 0) return null;
return parsed;
}
export function SleeptimeSelector({
initialSettings,
memfsEnabled,
onSave,
onCancel,
}: SleeptimeSelectorProps) {
const terminalWidth = useTerminalWidth();
const solidLine = SOLID_LINE.repeat(Math.max(terminalWidth, 10));
const initialState = useMemo(
() => parseInitialState(initialSettings),
[initialSettings],
);
const [trigger, setTrigger] = useState<ReflectionTrigger>(() => {
if (!memfsEnabled && initialState.trigger === "compaction-event") {
return "step-count";
}
return initialState.trigger;
});
const [behavior, setBehavior] = useState<ReflectionBehavior>(
initialState.behavior,
);
const [stepCountInput, setStepCountInput] = useState(initialState.stepCount);
const [focusRow, setFocusRow] = useState<FocusRow>("trigger");
const [validationError, setValidationError] = useState<string | null>(null);
const triggerOptions = useMemo(
() => getTriggerOptions(memfsEnabled),
[memfsEnabled],
);
const visibleRows = useMemo(() => {
const rows: FocusRow[] = ["trigger"];
if (memfsEnabled && trigger !== "off") {
rows.push("behavior");
}
if (trigger === "step-count") {
rows.push("step-count");
}
return rows;
}, [memfsEnabled, trigger]);
const isEditingStepCount =
focusRow === "step-count" && trigger === "step-count";
useEffect(() => {
if (!visibleRows.includes(focusRow)) {
setFocusRow(visibleRows[visibleRows.length - 1] ?? "trigger");
}
}, [focusRow, visibleRows]);
const saveSelection = () => {
if (trigger === "step-count") {
const stepCount = parseStepCount(stepCountInput);
if (stepCount === null) {
setValidationError("must be a positive integer");
return;
}
onSave({
trigger,
behavior: memfsEnabled ? behavior : "reminder",
stepCount,
});
return;
}
const fallbackStepCount =
parseStepCount(stepCountInput) ?? Number(DEFAULT_STEP_COUNT);
onSave({
trigger,
behavior: memfsEnabled ? behavior : "reminder",
stepCount: fallbackStepCount,
});
};
useInput((input, key) => {
if (key.ctrl && input === "c") {
onCancel();
return;
}
if (key.escape) {
onCancel();
return;
}
if (key.return) {
saveSelection();
return;
}
if (key.upArrow || key.downArrow) {
if (visibleRows.length === 0) return;
setValidationError(null);
const direction = key.downArrow ? 1 : -1;
const currentIndex = visibleRows.indexOf(focusRow);
const safeIndex = currentIndex >= 0 ? currentIndex : 0;
const nextIndex =
(safeIndex + direction + visibleRows.length) % visibleRows.length;
const nextRow = visibleRows[nextIndex] ?? "trigger";
setFocusRow(nextRow);
return;
}
if (key.leftArrow || key.rightArrow || key.tab) {
setValidationError(null);
const direction: -1 | 1 = key.leftArrow ? -1 : 1;
if (focusRow === "trigger") {
setTrigger((prev) => cycleOption(triggerOptions, prev, direction));
} else if (focusRow === "behavior" && memfsEnabled && trigger !== "off") {
setBehavior((prev) => cycleOption(BEHAVIOR_OPTIONS, prev, direction));
}
return;
}
if (!isEditingStepCount) return;
if (key.backspace || key.delete) {
setStepCountInput((prev) => prev.slice(0, -1));
setValidationError(null);
return;
}
// Allow arbitrary typing and validate only when saving.
if (
input &&
input.length > 0 &&
!key.ctrl &&
!key.meta &&
!key.tab &&
!key.upArrow &&
!key.downArrow &&
!key.leftArrow &&
!key.rightArrow
) {
setStepCountInput((prev) => `${prev}${input}`);
setValidationError(null);
}
});
return (
<Box flexDirection="column">
<Text dimColor>{"> /sleeptime"}</Text>
<Text dimColor>{solidLine}</Text>
<Box height={1} />
<Text bold color={colors.selector.title}>
Configure your sleeptime (reflection) settings
</Text>
<Box height={1} />
{memfsEnabled ? (
<>
<Box flexDirection="row">
<Text>{focusRow === "trigger" ? "> " : " "}</Text>
<Text bold>Trigger:</Text>
<Text>{" "}</Text>
<Text
backgroundColor={
trigger === "off" ? colors.selector.itemHighlighted : undefined
}
color={trigger === "off" ? "black" : undefined}
bold={trigger === "off"}
>
{" Off "}
</Text>
<Text> </Text>
<Text
backgroundColor={
trigger === "step-count"
? colors.selector.itemHighlighted
: undefined
}
color={trigger === "step-count" ? "black" : undefined}
bold={trigger === "step-count"}
>
{" Step count "}
</Text>
<Text> </Text>
<Text
backgroundColor={
trigger === "compaction-event"
? colors.selector.itemHighlighted
: undefined
}
color={trigger === "compaction-event" ? "black" : undefined}
bold={trigger === "compaction-event"}
>
{" Compaction event "}
</Text>
</Box>
{trigger !== "off" && (
<>
<Box height={1} />
<Box flexDirection="row">
<Text>{focusRow === "behavior" ? "> " : " "}</Text>
<Text bold>Trigger behavior:</Text>
<Text>{" "}</Text>
<Text
backgroundColor={
behavior === "reminder"
? colors.selector.itemHighlighted
: undefined
}
color={behavior === "reminder" ? "black" : undefined}
bold={behavior === "reminder"}
>
{" Reminder "}
</Text>
<Text> </Text>
<Text
backgroundColor={
behavior === "auto-launch"
? colors.selector.itemHighlighted
: undefined
}
color={behavior === "auto-launch" ? "black" : undefined}
bold={behavior === "auto-launch"}
>
{" Auto-launch "}
</Text>
</Box>
</>
)}
{trigger === "step-count" && (
<>
<Box height={1} />
<Box flexDirection="row">
<Text>{focusRow === "step-count" ? "> " : " "}</Text>
<Text bold>Step count: </Text>
<Text>{stepCountInput}</Text>
{isEditingStepCount && <Text></Text>}
{validationError && (
<Text color={colors.error.text}>
{` (error: ${validationError})`}
</Text>
)}
</Box>
</>
)}
</>
) : (
<>
<Box flexDirection="row">
<Text>{focusRow === "trigger" ? "> " : " "}</Text>
<Text bold>Trigger:</Text>
<Text>{" "}</Text>
<Text
backgroundColor={
trigger === "off" ? colors.selector.itemHighlighted : undefined
}
color={trigger === "off" ? "black" : undefined}
bold={trigger === "off"}
>
{" Off "}
</Text>
<Text> </Text>
<Text
backgroundColor={
trigger === "step-count"
? colors.selector.itemHighlighted
: undefined
}
color={trigger === "step-count" ? "black" : undefined}
bold={trigger === "step-count"}
>
{" Step count "}
</Text>
</Box>
{trigger === "step-count" && (
<>
<Box height={1} />
<Box flexDirection="row">
<Text>{focusRow === "step-count" ? "> " : " "}</Text>
<Text bold>Step count: </Text>
<Text>{stepCountInput}</Text>
{isEditingStepCount && <Text></Text>}
{validationError && (
<Text color={colors.error.text}>
{` (error: ${validationError})`}
</Text>
)}
</Box>
</>
)}
</>
)}
<Box height={1} />
<Text dimColor>
{" Enter to save · ↑↓ rows · ←→/Tab options · Esc cancel"}
</Text>
</Box>
);
}

View File

@@ -18,6 +18,34 @@ import { MAX_CONTEXT_HISTORY } from "./contextTracker";
import { findLastSafeSplitPoint } from "./markdownSplit";
import { isShellTool } from "./toolNameMapping";
type CompactionSummaryMessageChunk = {
message_type: "summary_message";
id?: string;
otid?: string;
summary?: string;
compaction_stats?: {
trigger?: string;
context_tokens_before?: number;
context_tokens_after?: number;
context_window?: number;
messages_count_before?: number;
messages_count_after?: number;
};
};
type CompactionEventMessageChunk = {
message_type: "event_message";
id?: string;
otid?: string;
event_type?: string;
event_data?: Record<string, unknown>;
};
type StreamingChunk =
| LettaStreamingResponse
| CompactionSummaryMessageChunk
| CompactionEventMessageChunk;
// Constants for streaming output
const MAX_TAIL_LINES = 5;
const MAX_BUFFER_SIZE = 100_000; // 100KB
@@ -473,7 +501,7 @@ function trySplitContent(
// Feed one SDK chunk; mutate buffers in place.
export function onChunk(
b: Buffers,
chunk: LettaStreamingResponse,
chunk: StreamingChunk,
ctx?: ContextTracker,
) {
// Skip processing if stream was interrupted mid-turn. handleInterrupt already
@@ -920,6 +948,7 @@ export function onChunk(
if (ctx) {
ctx.pendingCompaction = true;
ctx.pendingSkillsReinject = true;
ctx.pendingReflectionTrigger = true;
}
break;
}

View File

@@ -18,6 +18,8 @@ export type ContextTracker = {
pendingCompaction: boolean;
/** Set when compaction happens; consumed by the next user message to reinject skills reminder */
pendingSkillsReinject: boolean;
/** Set when compaction happens; consumed by the next user message to trigger memory reminder/spawn */
pendingReflectionTrigger: boolean;
};
export function createContextTracker(): ContextTracker {
@@ -27,6 +29,7 @@ export function createContextTracker(): ContextTracker {
currentTurnId: 0, // simple in-memory counter for now
pendingCompaction: false,
pendingSkillsReinject: false,
pendingReflectionTrigger: false,
};
}
@@ -34,4 +37,7 @@ export function createContextTracker(): ContextTracker {
export function resetContextHistory(ct: ContextTracker): void {
ct.lastContextTokens = 0;
ct.contextTokensHistory = [];
ct.pendingCompaction = false;
ct.pendingSkillsReinject = false;
ct.pendingReflectionTrigger = false;
}

View File

@@ -7,24 +7,197 @@ import { debugLog } from "../../utils/debug";
// Memory reminder interval presets
const MEMORY_INTERVAL_FREQUENT = 5;
const MEMORY_INTERVAL_OCCASIONAL = 10;
const DEFAULT_STEP_COUNT = 25;
export type MemoryReminderMode =
| number
| null
| "compaction"
| "auto-compaction";
export type ReflectionTrigger = "off" | "step-count" | "compaction-event";
export type ReflectionBehavior = "reminder" | "auto-launch";
export interface ReflectionSettings {
trigger: ReflectionTrigger;
behavior: ReflectionBehavior;
stepCount: number;
}
const DEFAULT_REFLECTION_SETTINGS: ReflectionSettings = {
trigger: "step-count",
behavior: "reminder",
stepCount: DEFAULT_STEP_COUNT,
};
function isValidStepCount(value: unknown): value is number {
return (
typeof value === "number" &&
Number.isFinite(value) &&
Number.isInteger(value) &&
value > 0
);
}
function normalizeStepCount(value: unknown, fallback: number): number {
return isValidStepCount(value) ? value : fallback;
}
function normalizeTrigger(
value: unknown,
fallback: ReflectionTrigger,
): ReflectionTrigger {
if (
value === "off" ||
value === "step-count" ||
value === "compaction-event"
) {
return value;
}
return fallback;
}
function normalizeBehavior(
value: unknown,
fallback: ReflectionBehavior,
): ReflectionBehavior {
if (value === "reminder" || value === "auto-launch") {
return value;
}
return fallback;
}
function applyExplicitReflectionOverrides(
base: ReflectionSettings,
raw: {
reflectionTrigger?: unknown;
reflectionBehavior?: unknown;
reflectionStepCount?: unknown;
},
): ReflectionSettings {
return {
trigger: normalizeTrigger(raw.reflectionTrigger, base.trigger),
behavior: normalizeBehavior(raw.reflectionBehavior, base.behavior),
stepCount: normalizeStepCount(raw.reflectionStepCount, base.stepCount),
};
}
function legacyModeToReflectionSettings(
mode: MemoryReminderMode | undefined,
): ReflectionSettings {
if (typeof mode === "number") {
return {
trigger: "step-count",
behavior: "reminder",
stepCount: normalizeStepCount(mode, DEFAULT_STEP_COUNT),
};
}
if (mode === null) {
return {
trigger: "off",
behavior: DEFAULT_REFLECTION_SETTINGS.behavior,
stepCount: DEFAULT_REFLECTION_SETTINGS.stepCount,
};
}
if (mode === "compaction") {
return {
trigger: "compaction-event",
behavior: "reminder",
stepCount: DEFAULT_REFLECTION_SETTINGS.stepCount,
};
}
if (mode === "auto-compaction") {
return {
trigger: "compaction-event",
behavior: "auto-launch",
stepCount: DEFAULT_REFLECTION_SETTINGS.stepCount,
};
}
return { ...DEFAULT_REFLECTION_SETTINGS };
}
export function reflectionSettingsToLegacyMode(
settings: ReflectionSettings,
): MemoryReminderMode {
if (settings.trigger === "off") {
return null;
}
if (settings.trigger === "compaction-event") {
return settings.behavior === "auto-launch"
? "auto-compaction"
: "compaction";
}
return normalizeStepCount(settings.stepCount, DEFAULT_STEP_COUNT);
}
/**
* Get the effective memory reminder interval (local setting takes precedence over global)
* @returns The memory interval setting, or null if disabled
* Get effective reflection settings (local overrides global with legacy fallback).
*/
function getMemoryInterval(): number | null {
export function getReflectionSettings(): ReflectionSettings {
const globalSettings = settingsManager.getSettings();
let resolved = legacyModeToReflectionSettings(
globalSettings.memoryReminderInterval,
);
resolved = applyExplicitReflectionOverrides(resolved, globalSettings);
// Check local settings first (may not be loaded, so catch errors)
try {
const localSettings = settingsManager.getLocalProjectSettings();
if (localSettings.memoryReminderInterval !== undefined) {
return localSettings.memoryReminderInterval;
resolved = legacyModeToReflectionSettings(
localSettings.memoryReminderInterval,
);
}
resolved = applyExplicitReflectionOverrides(resolved, localSettings);
} catch {
// Local settings not loaded, fall through to global
}
// Fall back to global setting
return settingsManager.getSetting("memoryReminderInterval");
return resolved;
}
/**
* Legacy mode view used by existing call sites while migrating to split fields.
*/
export function getMemoryReminderMode(): MemoryReminderMode {
return reflectionSettingsToLegacyMode(getReflectionSettings());
}
async function buildMemfsAwareMemoryReminder(
agentId: string,
trigger: "interval" | "compaction",
): Promise<string> {
if (settingsManager.isMemfsEnabled(agentId)) {
debugLog(
"memory",
`Reflection reminder fired (${trigger}, agent ${agentId})`,
);
const { MEMORY_REFLECTION_REMINDER } = await import(
"../../agent/promptAssets.js"
);
return MEMORY_REFLECTION_REMINDER;
}
debugLog(
"memory",
`Memory check reminder fired (${trigger}, agent ${agentId})`,
);
const { MEMORY_CHECK_REMINDER } = await import("../../agent/promptAssets.js");
return MEMORY_CHECK_REMINDER;
}
/**
* Build a compaction-triggered memory reminder. Uses the same memfs-aware
* selection as interval reminders.
*/
export async function buildCompactionMemoryReminder(
agentId: string,
): Promise<string> {
return buildMemfsAwareMemoryReminder(agentId, "compaction");
}
/**
@@ -43,28 +216,22 @@ export async function buildMemoryReminder(
turnCount: number,
agentId: string,
): Promise<string> {
const memoryInterval = getMemoryInterval();
if (memoryInterval && turnCount > 0 && turnCount % memoryInterval === 0) {
if (settingsManager.isMemfsEnabled(agentId)) {
debugLog(
"memory",
`Reflection reminder fired (turn ${turnCount}, agent ${agentId})`,
);
const { MEMORY_REFLECTION_REMINDER } = await import(
"../../agent/promptAssets.js"
);
return MEMORY_REFLECTION_REMINDER;
}
const reflectionSettings = getReflectionSettings();
if (reflectionSettings.trigger !== "step-count") {
return "";
}
if (
turnCount > 0 &&
turnCount %
normalizeStepCount(reflectionSettings.stepCount, DEFAULT_STEP_COUNT) ===
0
) {
debugLog(
"memory",
`Memory check reminder fired (turn ${turnCount}, agent ${agentId})`,
`Turn-based memory reminder fired (turn ${turnCount}, interval ${reflectionSettings.stepCount}, agent ${agentId})`,
);
const { MEMORY_CHECK_REMINDER } = await import(
"../../agent/promptAssets.js"
);
return MEMORY_CHECK_REMINDER;
return buildMemfsAwareMemoryReminder(agentId, "interval");
}
return "";
@@ -103,11 +270,17 @@ export function parseMemoryPreference(
if (answer.includes("frequent")) {
settingsManager.updateLocalProjectSettings({
memoryReminderInterval: MEMORY_INTERVAL_FREQUENT,
reflectionTrigger: "step-count",
reflectionBehavior: "reminder",
reflectionStepCount: MEMORY_INTERVAL_FREQUENT,
});
return true;
} else if (answer.includes("occasional")) {
settingsManager.updateLocalProjectSettings({
memoryReminderInterval: MEMORY_INTERVAL_OCCASIONAL,
reflectionTrigger: "step-count",
reflectionBehavior: "reminder",
reflectionStepCount: MEMORY_INTERVAL_OCCASIONAL,
});
return true;
}