Merge ecb351b901 into 4583f88626
This commit is contained in:
commit
bd1ea0e3b1
@ -1,4 +1,5 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
|
import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
|
||||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||||
import { isCliProvider } from "../../agents/model-selection.js";
|
import { isCliProvider } from "../../agents/model-selection.js";
|
||||||
@ -20,6 +21,7 @@ import {
|
|||||||
resolveMemoryFlushContextWindowTokens,
|
resolveMemoryFlushContextWindowTokens,
|
||||||
resolveMemoryFlushSettings,
|
resolveMemoryFlushSettings,
|
||||||
shouldRunMemoryFlush,
|
shouldRunMemoryFlush,
|
||||||
|
shouldRunHardThresholdCommand,
|
||||||
} from "./memory-flush.js";
|
} from "./memory-flush.js";
|
||||||
import type { FollowupRun } from "./queue.js";
|
import type { FollowupRun } from "./queue.js";
|
||||||
import { incrementCompactionCount } from "./session-updates.js";
|
import { incrementCompactionCount } from "./session-updates.js";
|
||||||
@ -71,6 +73,63 @@ export async function runMemoryFlushIfNeeded(params: {
|
|||||||
|
|
||||||
if (!shouldFlushMemory) return params.sessionEntry;
|
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;
|
let activeSessionEntry = params.sessionEntry;
|
||||||
const activeSessionStore = params.sessionStore;
|
const activeSessionStore = params.sessionStore;
|
||||||
const flushRunId = crypto.randomUUID();
|
const flushRunId = crypto.randomUUID();
|
||||||
@ -191,3 +250,44 @@ export async function runMemoryFlushIfNeeded(params: {
|
|||||||
|
|
||||||
return activeSessionEntry;
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -25,6 +25,8 @@ export type MemoryFlushSettings = {
|
|||||||
prompt: string;
|
prompt: string;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
reserveTokensFloor: number;
|
reserveTokensFloor: number;
|
||||||
|
hardThresholdTokens: number | null;
|
||||||
|
hardThresholdCommand: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeNonNegativeInt = (value: unknown): number | null => {
|
const normalizeNonNegativeInt = (value: unknown): number | null => {
|
||||||
@ -44,6 +46,8 @@ export function resolveMemoryFlushSettings(cfg?: MoltbotConfig): MemoryFlushSett
|
|||||||
const reserveTokensFloor =
|
const reserveTokensFloor =
|
||||||
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 hardThresholdTokens = normalizeNonNegativeInt(defaults?.hardThresholdTokens);
|
||||||
|
const hardThresholdCommand = defaults?.hardThresholdCommand?.trim() || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
@ -51,6 +55,8 @@ export function resolveMemoryFlushSettings(cfg?: MoltbotConfig): MemoryFlushSett
|
|||||||
prompt: ensureNoReplyHint(prompt),
|
prompt: ensureNoReplyHint(prompt),
|
||||||
systemPrompt: ensureNoReplyHint(systemPrompt),
|
systemPrompt: ensureNoReplyHint(systemPrompt),
|
||||||
reserveTokensFloor,
|
reserveTokensFloor,
|
||||||
|
hardThresholdTokens,
|
||||||
|
hardThresholdCommand,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,3 +97,35 @@ export function shouldRunMemoryFlush(params: {
|
|||||||
|
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -259,4 +259,8 @@ 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;
|
||||||
|
/** 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;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -97,6 +97,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(),
|
||||||
|
hardThresholdTokens: z.number().int().nonnegative().optional(),
|
||||||
|
hardThresholdCommand: z.string().optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user