feat(memoryFlush): add resetSession option to reset session after memory flush

Adds a new 'resetSession' config option to memoryFlush that, when enabled,
resets the session after the memory flush turn completes instead of
continuing to auto-compaction.

This allows agents to:
1. Write both daily notes (historical log) AND a handover file (minimum viable memory)
2. Get a fresh session that loads only the curated handover context
3. Continue work with lean, focused context instead of compacted history

Config example:
```yaml
agents:
  defaults:
    compaction:
      memoryFlush:
        enabled: true
        softThresholdTokens: 90000
        resetSession: true
        prompt: 'Write handover to memory/handover.md...'
```

When resetSession is true:
- Memory flush turn runs as normal
- Agent writes its memory/handover files
- Session is reset (new session ID, fresh transcript)
- Next turn starts fresh, loading handover.md via AGENTS.md
- Verbose mode shows '📝 Memory flush complete. Session reset.'
This commit is contained in:
Kai Valo 2026-01-26 15:09:48 +00:00
parent 2a4ccb624a
commit 5b325ae413
5 changed files with 71 additions and 6 deletions

View File

@ -17,6 +17,7 @@ import type { VerboseLevel } from "../thinking.js";
import type { GetReplyOptions } from "../types.js"; import type { GetReplyOptions } from "../types.js";
import { buildThreadingToolContext, resolveEnforceFinalTag } from "./agent-runner-utils.js"; import { buildThreadingToolContext, resolveEnforceFinalTag } from "./agent-runner-utils.js";
import { import {
type MemoryFlushResult,
resolveMemoryFlushContextWindowTokens, resolveMemoryFlushContextWindowTokens,
resolveMemoryFlushSettings, resolveMemoryFlushSettings,
shouldRunMemoryFlush, shouldRunMemoryFlush,
@ -37,9 +38,9 @@ export async function runMemoryFlushIfNeeded(params: {
sessionKey?: string; sessionKey?: string;
storePath?: string; storePath?: string;
isHeartbeat: boolean; isHeartbeat: boolean;
}): Promise<SessionEntry | undefined> { }): Promise<MemoryFlushResult> {
const memoryFlushSettings = resolveMemoryFlushSettings(params.cfg); const memoryFlushSettings = resolveMemoryFlushSettings(params.cfg);
if (!memoryFlushSettings) return params.sessionEntry; if (!memoryFlushSettings) return { sessionEntry: params.sessionEntry, shouldResetSession: false };
const memoryFlushWritable = (() => { const memoryFlushWritable = (() => {
if (!params.sessionKey) return true; if (!params.sessionKey) return true;
@ -69,7 +70,7 @@ export async function runMemoryFlushIfNeeded(params: {
softThresholdTokens: memoryFlushSettings.softThresholdTokens, softThresholdTokens: memoryFlushSettings.softThresholdTokens,
}); });
if (!shouldFlushMemory) return params.sessionEntry; if (!shouldFlushMemory) return { sessionEntry: params.sessionEntry, shouldResetSession: false };
let activeSessionEntry = params.sessionEntry; let activeSessionEntry = params.sessionEntry;
const activeSessionStore = params.sessionStore; const activeSessionStore = params.sessionStore;
@ -182,7 +183,12 @@ export async function runMemoryFlushIfNeeded(params: {
} }
} catch (err) { } catch (err) {
logVerbose(`memory flush run failed: ${String(err)}`); logVerbose(`memory flush run failed: ${String(err)}`);
// On failure, don't reset session
return { sessionEntry: activeSessionEntry, shouldResetSession: false };
} }
return activeSessionEntry; return {
sessionEntry: activeSessionEntry,
shouldResetSession: memoryFlushSettings.resetSession,
};
} }

View File

@ -198,7 +198,7 @@ export async function runReplyAgent(params: {
await typingSignals.signalRunStart(); await typingSignals.signalRunStart();
activeSessionEntry = await runMemoryFlushIfNeeded({ const memoryFlushResult = await runMemoryFlushIfNeeded({
cfg, cfg,
followupRun, followupRun,
sessionCtx, sessionCtx,
@ -212,6 +212,48 @@ export async function runReplyAgent(params: {
storePath, storePath,
isHeartbeat, isHeartbeat,
}); });
activeSessionEntry = memoryFlushResult.sessionEntry;
// Handle session reset after memory flush if configured
let memoryFlushResetCompleted = false;
if (memoryFlushResult.shouldResetSession && sessionKey && activeSessionStore && storePath) {
const prevEntry = activeSessionStore[sessionKey] ?? activeSessionEntry;
if (prevEntry) {
const nextSessionId = crypto.randomUUID();
const agentId = resolveAgentIdFromSessionKey(sessionKey);
const nextSessionFile = resolveSessionTranscriptPath(
nextSessionId,
agentId,
sessionCtx.MessageThreadId,
);
const nextEntry: SessionEntry = {
...prevEntry,
sessionId: nextSessionId,
updatedAt: Date.now(),
systemSent: false,
abortedLastRun: false,
sessionFile: nextSessionFile,
};
activeSessionStore[sessionKey] = nextEntry;
try {
await updateSessionStore(storePath, (store) => {
store[sessionKey] = nextEntry;
});
followupRun.run.sessionId = nextSessionId;
followupRun.run.sessionFile = nextSessionFile;
activeSessionEntry = nextEntry;
activeIsNewSession = true;
memoryFlushResetCompleted = true;
defaultRuntime.error(
`Memory flush complete. Session reset: ${sessionKey} -> ${nextSessionId}`,
);
} catch (err) {
defaultRuntime.error(
`Failed to persist session reset after memory flush (${sessionKey}): ${String(err)}`,
);
}
}
}
const runFollowupTurn = createFollowupRunner({ const runFollowupTurn = createFollowupRunner({
opts, opts,
@ -495,7 +537,9 @@ export async function runReplyAgent(params: {
finalPayloads = [{ text: `🧹 Auto-compaction complete${suffix}.` }, ...finalPayloads]; finalPayloads = [{ text: `🧹 Auto-compaction complete${suffix}.` }, ...finalPayloads];
} }
} }
if (verboseEnabled && activeIsNewSession) { if (memoryFlushResetCompleted && verboseEnabled) {
finalPayloads = [{ text: `📝 Memory flush complete. Session reset.` }, ...finalPayloads];
} else if (verboseEnabled && activeIsNewSession) {
finalPayloads = [{ text: `🧭 New session: ${followupRun.run.sessionId}` }, ...finalPayloads]; finalPayloads = [{ text: `🧭 New session: ${followupRun.run.sessionId}` }, ...finalPayloads];
} }
if (responseUsageLine) { if (responseUsageLine) {

View File

@ -5,6 +5,12 @@ import type { ClawdbotConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js"; import type { SessionEntry } from "../../config/sessions.js";
import { SILENT_REPLY_TOKEN } from "../tokens.js"; import { SILENT_REPLY_TOKEN } from "../tokens.js";
export type MemoryFlushResult = {
sessionEntry: SessionEntry | undefined;
/** True if the session should be reset after memory flush. */
shouldResetSession: boolean;
};
export const DEFAULT_MEMORY_FLUSH_SOFT_TOKENS = 4000; export const DEFAULT_MEMORY_FLUSH_SOFT_TOKENS = 4000;
export const DEFAULT_MEMORY_FLUSH_PROMPT = [ export const DEFAULT_MEMORY_FLUSH_PROMPT = [
@ -25,6 +31,8 @@ export type MemoryFlushSettings = {
prompt: string; prompt: string;
systemPrompt: string; systemPrompt: string;
reserveTokensFloor: number; reserveTokensFloor: number;
/** Reset the session after memory flush instead of continuing to compaction. */
resetSession: boolean;
}; };
const normalizeNonNegativeInt = (value: unknown): number | null => { const normalizeNonNegativeInt = (value: unknown): number | null => {
@ -45,12 +53,15 @@ export function resolveMemoryFlushSettings(cfg?: ClawdbotConfig): MemoryFlushSet
normalizeNonNegativeInt(cfg?.agents?.defaults?.compaction?.reserveTokensFloor) ?? normalizeNonNegativeInt(cfg?.agents?.defaults?.compaction?.reserveTokensFloor) ??
DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR; DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR;
const resetSession = defaults?.resetSession ?? false;
return { return {
enabled, enabled,
softThresholdTokens, softThresholdTokens,
prompt: ensureNoReplyHint(prompt), prompt: ensureNoReplyHint(prompt),
systemPrompt: ensureNoReplyHint(systemPrompt), systemPrompt: ensureNoReplyHint(systemPrompt),
reserveTokensFloor, reserveTokensFloor,
resetSession,
}; };
} }

View File

@ -257,4 +257,6 @@ export type AgentCompactionMemoryFlushConfig = {
prompt?: string; prompt?: string;
/** System prompt appended for the memory flush turn. */ /** System prompt appended for the memory flush turn. */
systemPrompt?: string; systemPrompt?: string;
/** Reset the session after memory flush instead of continuing to compaction (default: false). */
resetSession?: boolean;
}; };

View File

@ -96,6 +96,8 @@ export const AgentDefaultsSchema = z
softThresholdTokens: z.number().int().nonnegative().optional(), softThresholdTokens: z.number().int().nonnegative().optional(),
prompt: z.string().optional(), prompt: z.string().optional(),
systemPrompt: z.string().optional(), systemPrompt: z.string().optional(),
/** Reset the session after memory flush instead of continuing to compaction. */
resetSession: z.boolean().optional(),
}) })
.strict() .strict()
.optional(), .optional(),