This commit is contained in:
Claire 2026-01-30 00:25:08 +01:00 committed by GitHub
commit bd1ea0e3b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 144 additions and 0 deletions

View File

@ -1,4 +1,5 @@
import crypto from "node:crypto";
import { spawn } from "node:child_process";
import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
import { runWithModelFallback } from "../../agents/model-fallback.js";
import { isCliProvider } from "../../agents/model-selection.js";
@ -20,6 +21,7 @@ import {
resolveMemoryFlushContextWindowTokens,
resolveMemoryFlushSettings,
shouldRunMemoryFlush,
shouldRunHardThresholdCommand,
} from "./memory-flush.js";
import type { FollowupRun } from "./queue.js";
import { incrementCompactionCount } from "./session-updates.js";
@ -71,6 +73,63 @@ export async function runMemoryFlushIfNeeded(params: {
if (!shouldFlushMemory) return params.sessionEntry;
// Check if hard threshold is reached - auto-execute command without agent prompt
const contextWindowTokens = resolveMemoryFlushContextWindowTokens({
modelId: params.followupRun.run.model ?? params.defaultModel,
agentCfgContextTokens: params.agentCfgContextTokens,
});
const shouldRunHardCommand =
memoryFlushSettings.hardThresholdCommand &&
memoryFlushSettings.hardThresholdTokens &&
shouldRunHardThresholdCommand({
entry:
params.sessionEntry ??
(params.sessionKey ? params.sessionStore?.[params.sessionKey] : undefined),
contextWindowTokens,
reserveTokensFloor: memoryFlushSettings.reserveTokensFloor,
hardThresholdTokens: memoryFlushSettings.hardThresholdTokens,
});
if (shouldRunHardCommand && memoryFlushSettings.hardThresholdCommand) {
logVerbose(
`hard threshold reached, auto-executing: ${memoryFlushSettings.hardThresholdCommand}`,
);
try {
await runHardThresholdCommand(
memoryFlushSettings.hardThresholdCommand,
params.followupRun.run.workspaceDir,
);
logVerbose("hard threshold command completed");
// Update session metadata to mark flush completed
if (params.storePath && params.sessionKey) {
const compactionCount =
params.sessionEntry?.compactionCount ??
(params.sessionKey ? params.sessionStore?.[params.sessionKey]?.compactionCount : 0) ??
0;
try {
const updatedEntry = await updateSessionStoreEntry({
storePath: params.storePath,
sessionKey: params.sessionKey,
update: async () => ({
memoryFlushAt: Date.now(),
memoryFlushCompactionCount: compactionCount,
}),
});
if (updatedEntry) {
return updatedEntry;
}
} catch (err) {
logVerbose(`failed to persist hard threshold flush metadata: ${String(err)}`);
}
}
return params.sessionEntry;
} catch (err) {
logVerbose(`hard threshold command failed: ${String(err)}`);
// Fall through to soft threshold agent prompt
}
}
let activeSessionEntry = params.sessionEntry;
const activeSessionStore = params.sessionStore;
const flushRunId = crypto.randomUUID();
@ -191,3 +250,44 @@ export async function runMemoryFlushIfNeeded(params: {
return activeSessionEntry;
}
/**
* Execute the hard threshold command directly (no agent involvement).
* Runs the command in a shell with the workspace as cwd.
*/
async function runHardThresholdCommand(command: string, workspaceDir?: string): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn(command, [], {
shell: true,
cwd: workspaceDir || process.cwd(),
stdio: ["ignore", "pipe", "pipe"],
timeout: 30000, // 30 second timeout
});
let stdout = "";
let stderr = "";
child.stdout?.on("data", (data) => {
stdout += data.toString();
});
child.stderr?.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (code) => {
if (code === 0) {
if (stdout.trim()) {
logVerbose(`hard threshold command output: ${stdout.trim()}`);
}
resolve();
} else {
reject(new Error(`Command exited with code ${code}: ${stderr || stdout}`));
}
});
child.on("error", (err) => {
reject(err);
});
});
}

View File

@ -25,6 +25,8 @@ export type MemoryFlushSettings = {
prompt: string;
systemPrompt: string;
reserveTokensFloor: number;
hardThresholdTokens: number | null;
hardThresholdCommand: string | null;
};
const normalizeNonNegativeInt = (value: unknown): number | null => {
@ -44,6 +46,8 @@ export function resolveMemoryFlushSettings(cfg?: MoltbotConfig): MemoryFlushSett
const reserveTokensFloor =
normalizeNonNegativeInt(cfg?.agents?.defaults?.compaction?.reserveTokensFloor) ??
DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR;
const hardThresholdTokens = normalizeNonNegativeInt(defaults?.hardThresholdTokens);
const hardThresholdCommand = defaults?.hardThresholdCommand?.trim() || null;
return {
enabled,
@ -51,6 +55,8 @@ export function resolveMemoryFlushSettings(cfg?: MoltbotConfig): MemoryFlushSett
prompt: ensureNoReplyHint(prompt),
systemPrompt: ensureNoReplyHint(systemPrompt),
reserveTokensFloor,
hardThresholdTokens,
hardThresholdCommand,
};
}
@ -91,3 +97,35 @@ export function shouldRunMemoryFlush(params: {
return true;
}
/**
* Check if the hard threshold has been reached and the command should auto-execute.
* Returns true if totalTokens exceeds the hard threshold.
*/
export function shouldRunHardThresholdCommand(params: {
entry?: Pick<SessionEntry, "totalTokens" | "compactionCount" | "memoryFlushCompactionCount">;
contextWindowTokens: number;
reserveTokensFloor: number;
hardThresholdTokens: number | null;
}): boolean {
if (!params.hardThresholdTokens) return false;
const totalTokens = params.entry?.totalTokens;
if (!totalTokens || totalTokens <= 0) return false;
const contextWindow = Math.max(1, Math.floor(params.contextWindowTokens));
const reserveTokens = Math.max(0, Math.floor(params.reserveTokensFloor));
const hardThreshold = Math.max(0, Math.floor(params.hardThresholdTokens));
const threshold = Math.max(0, contextWindow - reserveTokens - hardThreshold);
if (threshold <= 0) return false;
if (totalTokens < threshold) return false;
// Check if we already ran the hard flush this compaction cycle
const compactionCount = params.entry?.compactionCount ?? 0;
const lastFlushAt = params.entry?.memoryFlushCompactionCount;
if (typeof lastFlushAt === "number" && lastFlushAt === compactionCount) {
return false;
}
return true;
}

View File

@ -259,4 +259,8 @@ export type AgentCompactionMemoryFlushConfig = {
prompt?: string;
/** System prompt appended for the memory flush turn. */
systemPrompt?: string;
/** Auto-execute hardThresholdCommand when context exceeds this many tokens (no agent prompt). */
hardThresholdTokens?: number;
/** Shell command to auto-execute when hard threshold is reached (e.g., "kernle -a agent checkpoint save 'auto-backup'"). */
hardThresholdCommand?: string;
};

View File

@ -97,6 +97,8 @@ export const AgentDefaultsSchema = z
softThresholdTokens: z.number().int().nonnegative().optional(),
prompt: z.string().optional(),
systemPrompt: z.string().optional(),
hardThresholdTokens: z.number().int().nonnegative().optional(),
hardThresholdCommand: z.string().optional(),
})
.strict()
.optional(),