feat(compaction): add post-compaction recovery turn [AI-assisted]
Adds automatic recovery turn after context compaction to help agents restore working memory from daily logs. Co-authored-by: Jarvis <257208152+jarvis-sam@users.noreply.github.com>
This commit is contained in:
parent
67f1402703
commit
e41d6d74af
@ -89,7 +89,24 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) {
|
|||||||
ctx.state.blockState.inlineCode = createInlineCodeState();
|
ctx.state.blockState.inlineCode = createInlineCodeState();
|
||||||
|
|
||||||
if (ctx.state.pendingCompactionRetry > 0) {
|
if (ctx.state.pendingCompactionRetry > 0) {
|
||||||
|
// A compaction retry was pending and this agent turn completed successfully.
|
||||||
|
// Emit a compaction end event with willRetry=false so callers know compaction
|
||||||
|
// ultimately succeeded (triggering post-compaction recovery turns, etc.).
|
||||||
|
const wasRetrying = ctx.state.pendingCompactionRetry > 0;
|
||||||
ctx.resolveCompactionRetry();
|
ctx.resolveCompactionRetry();
|
||||||
|
// Only emit success event when all retries are resolved
|
||||||
|
if (wasRetrying && ctx.state.pendingCompactionRetry === 0 && !ctx.state.compactionInFlight) {
|
||||||
|
ctx.log.debug(`embedded run compaction retry success: runId=${ctx.params.runId}`);
|
||||||
|
emitAgentEvent({
|
||||||
|
runId: ctx.params.runId,
|
||||||
|
stream: "compaction",
|
||||||
|
data: { phase: "end", willRetry: false },
|
||||||
|
});
|
||||||
|
void ctx.params.onAgentEvent?.({
|
||||||
|
stream: "compaction",
|
||||||
|
data: { phase: "end", willRetry: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.maybeResolveCompactionWait();
|
ctx.maybeResolveCompactionWait();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import {
|
|||||||
resolveMemoryFlushSettings,
|
resolveMemoryFlushSettings,
|
||||||
shouldRunMemoryFlush,
|
shouldRunMemoryFlush,
|
||||||
} from "./memory-flush.js";
|
} from "./memory-flush.js";
|
||||||
|
import { resolvePostCompactionSettings, shouldRunPostCompaction } from "./post-compaction.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";
|
||||||
|
|
||||||
@ -189,5 +190,171 @@ export async function runMemoryFlushIfNeeded(params: {
|
|||||||
logVerbose(`memory flush run failed: ${String(err)}`);
|
logVerbose(`memory flush run failed: ${String(err)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run post-compaction recovery if needed
|
||||||
|
if (memoryCompactionCompleted) {
|
||||||
|
activeSessionEntry =
|
||||||
|
(await runPostCompactionRecoveryIfNeeded({
|
||||||
|
cfg: params.cfg,
|
||||||
|
followupRun: params.followupRun,
|
||||||
|
sessionCtx: params.sessionCtx,
|
||||||
|
opts: params.opts,
|
||||||
|
defaultModel: params.defaultModel,
|
||||||
|
agentCfgContextTokens: params.agentCfgContextTokens,
|
||||||
|
resolvedVerboseLevel: params.resolvedVerboseLevel,
|
||||||
|
sessionEntry: activeSessionEntry,
|
||||||
|
sessionStore: params.sessionStore,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
storePath: params.storePath,
|
||||||
|
isHeartbeat: params.isHeartbeat,
|
||||||
|
memoryCompactionCompleted,
|
||||||
|
})) ?? activeSessionEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeSessionEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runPostCompactionRecoveryIfNeeded(params: {
|
||||||
|
cfg: MoltbotConfig;
|
||||||
|
followupRun: FollowupRun;
|
||||||
|
sessionCtx: TemplateContext;
|
||||||
|
opts?: GetReplyOptions;
|
||||||
|
defaultModel: string;
|
||||||
|
agentCfgContextTokens?: number;
|
||||||
|
resolvedVerboseLevel: VerboseLevel;
|
||||||
|
sessionEntry?: SessionEntry;
|
||||||
|
sessionStore?: Record<string, SessionEntry>;
|
||||||
|
sessionKey?: string;
|
||||||
|
storePath?: string;
|
||||||
|
isHeartbeat: boolean;
|
||||||
|
memoryCompactionCompleted: boolean;
|
||||||
|
}): Promise<SessionEntry | undefined> {
|
||||||
|
const postCompactionSettings = resolvePostCompactionSettings(params.cfg);
|
||||||
|
if (!postCompactionSettings) return params.sessionEntry;
|
||||||
|
|
||||||
|
const postCompactionWritable = (() => {
|
||||||
|
if (!params.sessionKey) return true;
|
||||||
|
const runtime = resolveSandboxRuntimeStatus({
|
||||||
|
cfg: params.cfg,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
});
|
||||||
|
if (!runtime.sandboxed) return true;
|
||||||
|
const sandboxCfg = resolveSandboxConfigForAgent(params.cfg, runtime.agentId);
|
||||||
|
return sandboxCfg.workspaceAccess === "rw";
|
||||||
|
})();
|
||||||
|
|
||||||
|
const shouldRunRecovery =
|
||||||
|
postCompactionSettings &&
|
||||||
|
postCompactionWritable &&
|
||||||
|
!params.isHeartbeat &&
|
||||||
|
!isCliProvider(params.followupRun.run.provider, params.cfg) &&
|
||||||
|
shouldRunPostCompaction({
|
||||||
|
entry:
|
||||||
|
params.sessionEntry ??
|
||||||
|
(params.sessionKey ? params.sessionStore?.[params.sessionKey] : undefined),
|
||||||
|
memoryCompactionCompleted: params.memoryCompactionCompleted,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shouldRunRecovery) return params.sessionEntry;
|
||||||
|
|
||||||
|
let activeSessionEntry = params.sessionEntry;
|
||||||
|
const recoveryRunId = crypto.randomUUID();
|
||||||
|
if (params.sessionKey) {
|
||||||
|
registerAgentRunContext(recoveryRunId, {
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
verboseLevel: params.resolvedVerboseLevel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const recoverySystemPrompt = [
|
||||||
|
params.followupRun.run.extraSystemPrompt,
|
||||||
|
postCompactionSettings.systemPrompt,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runWithModelFallback({
|
||||||
|
cfg: params.followupRun.run.config,
|
||||||
|
provider: params.followupRun.run.provider,
|
||||||
|
model: params.followupRun.run.model,
|
||||||
|
agentDir: params.followupRun.run.agentDir,
|
||||||
|
fallbacksOverride: resolveAgentModelFallbacksOverride(
|
||||||
|
params.followupRun.run.config,
|
||||||
|
resolveAgentIdFromSessionKey(params.followupRun.run.sessionKey),
|
||||||
|
),
|
||||||
|
run: (provider, model) => {
|
||||||
|
const authProfileId =
|
||||||
|
provider === params.followupRun.run.provider
|
||||||
|
? params.followupRun.run.authProfileId
|
||||||
|
: undefined;
|
||||||
|
return runEmbeddedPiAgent({
|
||||||
|
sessionId: params.followupRun.run.sessionId,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
messageProvider: params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||||
|
agentAccountId: params.sessionCtx.AccountId,
|
||||||
|
messageTo: params.sessionCtx.OriginatingTo ?? params.sessionCtx.To,
|
||||||
|
messageThreadId: params.sessionCtx.MessageThreadId ?? undefined,
|
||||||
|
// Provider threading context for tool auto-injection
|
||||||
|
...buildThreadingToolContext({
|
||||||
|
sessionCtx: params.sessionCtx,
|
||||||
|
config: params.followupRun.run.config,
|
||||||
|
hasRepliedRef: params.opts?.hasRepliedRef,
|
||||||
|
}),
|
||||||
|
senderId: params.sessionCtx.SenderId?.trim() || undefined,
|
||||||
|
senderName: params.sessionCtx.SenderName?.trim() || undefined,
|
||||||
|
senderUsername: params.sessionCtx.SenderUsername?.trim() || undefined,
|
||||||
|
senderE164: params.sessionCtx.SenderE164?.trim() || undefined,
|
||||||
|
sessionFile: params.followupRun.run.sessionFile,
|
||||||
|
workspaceDir: params.followupRun.run.workspaceDir,
|
||||||
|
agentDir: params.followupRun.run.agentDir,
|
||||||
|
config: params.followupRun.run.config,
|
||||||
|
skillsSnapshot: params.followupRun.run.skillsSnapshot,
|
||||||
|
prompt: postCompactionSettings.prompt,
|
||||||
|
extraSystemPrompt: recoverySystemPrompt,
|
||||||
|
ownerNumbers: params.followupRun.run.ownerNumbers,
|
||||||
|
enforceFinalTag: resolveEnforceFinalTag(params.followupRun.run, provider),
|
||||||
|
provider,
|
||||||
|
model,
|
||||||
|
authProfileId,
|
||||||
|
authProfileIdSource: authProfileId
|
||||||
|
? params.followupRun.run.authProfileIdSource
|
||||||
|
: undefined,
|
||||||
|
thinkLevel: params.followupRun.run.thinkLevel,
|
||||||
|
verboseLevel: params.followupRun.run.verboseLevel,
|
||||||
|
reasoningLevel: params.followupRun.run.reasoningLevel,
|
||||||
|
execOverrides: params.followupRun.run.execOverrides,
|
||||||
|
bashElevated: params.followupRun.run.bashElevated,
|
||||||
|
timeoutMs: params.followupRun.run.timeoutMs,
|
||||||
|
runId: recoveryRunId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const postCompactionCompactionCount =
|
||||||
|
activeSessionEntry?.compactionCount ??
|
||||||
|
(params.sessionKey ? params.sessionStore?.[params.sessionKey]?.compactionCount : 0) ??
|
||||||
|
0;
|
||||||
|
|
||||||
|
if (params.storePath && params.sessionKey) {
|
||||||
|
try {
|
||||||
|
const updatedEntry = await updateSessionStoreEntry({
|
||||||
|
storePath: params.storePath,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
update: async () => ({
|
||||||
|
postCompactionAt: Date.now(),
|
||||||
|
postCompactionCompactionCount,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (updatedEntry) {
|
||||||
|
activeSessionEntry = updatedEntry;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logVerbose(`failed to persist post-compaction recovery metadata: ${String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logVerbose(`post-compaction recovery run failed: ${String(err)}`);
|
||||||
|
}
|
||||||
|
|
||||||
return activeSessionEntry;
|
return activeSessionEntry;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,10 @@ import {
|
|||||||
isAudioPayload,
|
isAudioPayload,
|
||||||
signalTypingIfNeeded,
|
signalTypingIfNeeded,
|
||||||
} from "./agent-runner-helpers.js";
|
} from "./agent-runner-helpers.js";
|
||||||
import { runMemoryFlushIfNeeded } from "./agent-runner-memory.js";
|
import {
|
||||||
|
runMemoryFlushIfNeeded,
|
||||||
|
runPostCompactionRecoveryIfNeeded,
|
||||||
|
} from "./agent-runner-memory.js";
|
||||||
import { buildReplyPayloads } from "./agent-runner-payloads.js";
|
import { buildReplyPayloads } from "./agent-runner-payloads.js";
|
||||||
import { appendUsageLine, formatResponseUsageLine } from "./agent-runner-utils.js";
|
import { appendUsageLine, formatResponseUsageLine } from "./agent-runner-utils.js";
|
||||||
import { createAudioAsVoiceBuffer, createBlockReplyPipeline } from "./block-reply-pipeline.js";
|
import { createAudioAsVoiceBuffer, createBlockReplyPipeline } from "./block-reply-pipeline.js";
|
||||||
@ -490,10 +493,32 @@ export async function runReplyAgent(params: {
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
storePath,
|
storePath,
|
||||||
});
|
});
|
||||||
|
// Refresh entry with updated compactionCount for post-compaction check
|
||||||
|
if (sessionKey && activeSessionStore?.[sessionKey]) {
|
||||||
|
activeSessionEntry = activeSessionStore[sessionKey];
|
||||||
|
}
|
||||||
if (verboseEnabled) {
|
if (verboseEnabled) {
|
||||||
const suffix = typeof count === "number" ? ` (count ${count})` : "";
|
const suffix = typeof count === "number" ? ` (count ${count})` : "";
|
||||||
finalPayloads = [{ text: `🧹 Auto-compaction complete${suffix}.` }, ...finalPayloads];
|
finalPayloads = [{ text: `🧹 Auto-compaction complete${suffix}.` }, ...finalPayloads];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run post-compaction recovery turn if enabled
|
||||||
|
activeSessionEntry =
|
||||||
|
(await runPostCompactionRecoveryIfNeeded({
|
||||||
|
cfg: followupRun.run.config,
|
||||||
|
followupRun,
|
||||||
|
sessionCtx,
|
||||||
|
opts,
|
||||||
|
defaultModel,
|
||||||
|
agentCfgContextTokens,
|
||||||
|
resolvedVerboseLevel,
|
||||||
|
sessionEntry: activeSessionEntry,
|
||||||
|
sessionStore: activeSessionStore,
|
||||||
|
sessionKey,
|
||||||
|
storePath,
|
||||||
|
isHeartbeat,
|
||||||
|
memoryCompactionCompleted: true,
|
||||||
|
})) ?? activeSessionEntry;
|
||||||
}
|
}
|
||||||
if (verboseEnabled && activeIsNewSession) {
|
if (verboseEnabled && activeIsNewSession) {
|
||||||
finalPayloads = [{ text: `🧭 New session: ${followupRun.run.sessionId}` }, ...finalPayloads];
|
finalPayloads = [{ text: `🧭 New session: ${followupRun.run.sessionId}` }, ...finalPayloads];
|
||||||
|
|||||||
145
src/auto-reply/reply/post-compaction.test.ts
Normal file
145
src/auto-reply/reply/post-compaction.test.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_POST_COMPACTION_PROMPT,
|
||||||
|
DEFAULT_POST_COMPACTION_SYSTEM_PROMPT,
|
||||||
|
resolvePostCompactionSettings,
|
||||||
|
shouldRunPostCompaction,
|
||||||
|
} from "./post-compaction.js";
|
||||||
|
|
||||||
|
describe("post-compaction settings", () => {
|
||||||
|
it("defaults to enabled with fallback prompt and system prompt", () => {
|
||||||
|
const settings = resolvePostCompactionSettings();
|
||||||
|
expect(settings).not.toBeNull();
|
||||||
|
expect(settings?.enabled).toBe(true);
|
||||||
|
expect(settings?.prompt.length).toBeGreaterThan(0);
|
||||||
|
expect(settings?.systemPrompt.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects disable flag", () => {
|
||||||
|
expect(
|
||||||
|
resolvePostCompactionSettings({
|
||||||
|
agents: {
|
||||||
|
defaults: { compaction: { postCompaction: { enabled: false } } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses default prompts when not configured", () => {
|
||||||
|
const settings = resolvePostCompactionSettings();
|
||||||
|
expect(settings?.prompt).toContain("Compaction completed");
|
||||||
|
expect(settings?.systemPrompt).toContain("Post-compaction recovery turn");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("appends NO_REPLY hint when missing", () => {
|
||||||
|
const settings = resolvePostCompactionSettings({
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
compaction: {
|
||||||
|
postCompaction: {
|
||||||
|
prompt: "Recovery turn now.",
|
||||||
|
systemPrompt: "Check memory.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(settings?.prompt).toContain("NO_REPLY");
|
||||||
|
expect(settings?.systemPrompt).toContain("NO_REPLY");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves NO_REPLY hint when already present", () => {
|
||||||
|
const settings = resolvePostCompactionSettings({
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
compaction: {
|
||||||
|
postCompaction: {
|
||||||
|
prompt: "Recovery turn. Reply NO_REPLY if nothing needed.",
|
||||||
|
systemPrompt: "Check memory. Use NO_REPLY when appropriate.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Should not double-append the hint
|
||||||
|
const promptHintCount = (settings?.prompt.match(/NO_REPLY/g) || []).length;
|
||||||
|
const systemHintCount = (settings?.systemPrompt.match(/NO_REPLY/g) || []).length;
|
||||||
|
expect(promptHintCount).toBe(1);
|
||||||
|
expect(systemHintCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shouldRunPostCompaction", () => {
|
||||||
|
it("requires memoryCompactionCompleted to be true", () => {
|
||||||
|
expect(
|
||||||
|
shouldRunPostCompaction({
|
||||||
|
entry: { compactionCount: 1 },
|
||||||
|
memoryCompactionCompleted: false,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runs when compaction just completed", () => {
|
||||||
|
expect(
|
||||||
|
shouldRunPostCompaction({
|
||||||
|
entry: { compactionCount: 1 },
|
||||||
|
memoryCompactionCompleted: true,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips when entry is missing but compaction completed", () => {
|
||||||
|
expect(
|
||||||
|
shouldRunPostCompaction({
|
||||||
|
entry: undefined,
|
||||||
|
memoryCompactionCompleted: true,
|
||||||
|
}),
|
||||||
|
).toBe(true); // Should still run even without entry
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips when already ran for current compaction count", () => {
|
||||||
|
expect(
|
||||||
|
shouldRunPostCompaction({
|
||||||
|
entry: {
|
||||||
|
compactionCount: 2,
|
||||||
|
postCompactionCompactionCount: 2,
|
||||||
|
},
|
||||||
|
memoryCompactionCompleted: true,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runs when compaction count increased since last post-compaction", () => {
|
||||||
|
expect(
|
||||||
|
shouldRunPostCompaction({
|
||||||
|
entry: {
|
||||||
|
compactionCount: 3,
|
||||||
|
postCompactionCompactionCount: 2,
|
||||||
|
},
|
||||||
|
memoryCompactionCompleted: true,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runs on first compaction when postCompactionCompactionCount is undefined", () => {
|
||||||
|
expect(
|
||||||
|
shouldRunPostCompaction({
|
||||||
|
entry: {
|
||||||
|
compactionCount: 1,
|
||||||
|
},
|
||||||
|
memoryCompactionCompleted: true,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("default prompts", () => {
|
||||||
|
it("DEFAULT_POST_COMPACTION_PROMPT includes memory file reference", () => {
|
||||||
|
expect(DEFAULT_POST_COMPACTION_PROMPT).toContain("memory/YYYY-MM-DD.md");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("DEFAULT_POST_COMPACTION_SYSTEM_PROMPT mentions memory files", () => {
|
||||||
|
expect(DEFAULT_POST_COMPACTION_SYSTEM_PROMPT).toContain("memory files");
|
||||||
|
});
|
||||||
|
});
|
||||||
57
src/auto-reply/reply/post-compaction.ts
Normal file
57
src/auto-reply/reply/post-compaction.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import type { MoltbotConfig } from "../../config/config.js";
|
||||||
|
import type { SessionEntry } from "../../config/sessions.js";
|
||||||
|
import { SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||||
|
|
||||||
|
export const DEFAULT_POST_COMPACTION_PROMPT = [
|
||||||
|
"Compaction completed.",
|
||||||
|
"Read memory/YYYY-MM-DD.md to recover context and continue any pending work.",
|
||||||
|
`Reply with ${SILENT_REPLY_TOKEN} if nothing requires attention.`,
|
||||||
|
].join(" ");
|
||||||
|
|
||||||
|
export const DEFAULT_POST_COMPACTION_SYSTEM_PROMPT = [
|
||||||
|
"Post-compaction recovery turn.",
|
||||||
|
"Check memory files for recent work context and verify if you were mid-task.",
|
||||||
|
].join(" ");
|
||||||
|
|
||||||
|
export type PostCompactionSettings = {
|
||||||
|
enabled: boolean;
|
||||||
|
prompt: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolvePostCompactionSettings(cfg?: MoltbotConfig): PostCompactionSettings | null {
|
||||||
|
const defaults = cfg?.agents?.defaults?.compaction?.postCompaction;
|
||||||
|
const enabled = defaults?.enabled ?? true;
|
||||||
|
if (!enabled) return null;
|
||||||
|
|
||||||
|
const prompt = defaults?.prompt?.trim() || DEFAULT_POST_COMPACTION_PROMPT;
|
||||||
|
const systemPrompt = defaults?.systemPrompt?.trim() || DEFAULT_POST_COMPACTION_SYSTEM_PROMPT;
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
prompt: ensureNoReplyHint(prompt),
|
||||||
|
systemPrompt: ensureNoReplyHint(systemPrompt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoReplyHint(text: string): string {
|
||||||
|
if (text.includes(SILENT_REPLY_TOKEN)) return text;
|
||||||
|
return `${text}\n\nIf no user-visible reply is needed, start with ${SILENT_REPLY_TOKEN}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldRunPostCompaction(params: {
|
||||||
|
entry?: Pick<SessionEntry, "compactionCount" | "postCompactionCompactionCount">;
|
||||||
|
memoryCompactionCompleted: boolean;
|
||||||
|
}): boolean {
|
||||||
|
if (!params.memoryCompactionCompleted) return false;
|
||||||
|
|
||||||
|
const compactionCount = params.entry?.compactionCount ?? 0;
|
||||||
|
const lastPostCompactionAt = params.entry?.postCompactionCompactionCount;
|
||||||
|
|
||||||
|
// Don't run if we've already run post-compaction for this compaction cycle
|
||||||
|
if (typeof lastPostCompactionAt === "number" && lastPostCompactionAt === compactionCount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@ -546,6 +546,30 @@ const FIELD_HELP: Record<string, string> = {
|
|||||||
"Minimum appended bytes before session transcripts trigger reindex (default: 100000).",
|
"Minimum appended bytes before session transcripts trigger reindex (default: 100000).",
|
||||||
"agents.defaults.memorySearch.sync.sessions.deltaMessages":
|
"agents.defaults.memorySearch.sync.sessions.deltaMessages":
|
||||||
"Minimum appended JSONL lines before session transcripts trigger reindex (default: 50).",
|
"Minimum appended JSONL lines before session transcripts trigger reindex (default: 50).",
|
||||||
|
"agents.defaults.compaction": "Compaction tuning and pre/post-compaction agentic turns.",
|
||||||
|
"agents.defaults.compaction.mode": 'Compaction summarization mode ("default" or "safeguard").',
|
||||||
|
"agents.defaults.compaction.reserveTokensFloor":
|
||||||
|
"Minimum reserve tokens enforced for Pi compaction (0 disables the floor).",
|
||||||
|
"agents.defaults.compaction.maxHistoryShare":
|
||||||
|
"Max share of context window for history during safeguard pruning (0.1–0.9, default 0.5).",
|
||||||
|
"agents.defaults.compaction.memoryFlush":
|
||||||
|
"Pre-compaction memory flush (agentic turn). Default: enabled.",
|
||||||
|
"agents.defaults.compaction.memoryFlush.enabled":
|
||||||
|
"Enable the pre-compaction memory flush (default: true).",
|
||||||
|
"agents.defaults.compaction.memoryFlush.softThresholdTokens":
|
||||||
|
"Run the memory flush when context is within this many tokens of the compaction threshold.",
|
||||||
|
"agents.defaults.compaction.memoryFlush.prompt":
|
||||||
|
"User prompt used for the memory flush turn (NO_REPLY is enforced if missing).",
|
||||||
|
"agents.defaults.compaction.memoryFlush.systemPrompt":
|
||||||
|
"System prompt appended for the memory flush turn.",
|
||||||
|
"agents.defaults.compaction.postCompaction":
|
||||||
|
"Post-compaction recovery turn (agentic turn). Default: enabled.",
|
||||||
|
"agents.defaults.compaction.postCompaction.enabled":
|
||||||
|
"Enable the post-compaction recovery turn (default: true).",
|
||||||
|
"agents.defaults.compaction.postCompaction.prompt":
|
||||||
|
"User prompt used for the post-compaction recovery turn (NO_REPLY is enforced if missing).",
|
||||||
|
"agents.defaults.compaction.postCompaction.systemPrompt":
|
||||||
|
"System prompt appended for the post-compaction recovery turn.",
|
||||||
"plugins.enabled": "Enable plugin/extension loading (default: true).",
|
"plugins.enabled": "Enable plugin/extension loading (default: true).",
|
||||||
"plugins.allow": "Optional allowlist of plugin ids; when set, only listed plugins load.",
|
"plugins.allow": "Optional allowlist of plugin ids; when set, only listed plugins load.",
|
||||||
"plugins.deny": "Optional denylist of plugin ids; deny wins over allowlist.",
|
"plugins.deny": "Optional denylist of plugin ids; deny wins over allowlist.",
|
||||||
|
|||||||
@ -77,6 +77,8 @@ export type SessionEntry = {
|
|||||||
compactionCount?: number;
|
compactionCount?: number;
|
||||||
memoryFlushAt?: number;
|
memoryFlushAt?: number;
|
||||||
memoryFlushCompactionCount?: number;
|
memoryFlushCompactionCount?: number;
|
||||||
|
postCompactionAt?: number;
|
||||||
|
postCompactionCompactionCount?: number;
|
||||||
cliSessionIds?: Record<string, string>;
|
cliSessionIds?: Record<string, string>;
|
||||||
claudeCliSessionId?: string;
|
claudeCliSessionId?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
|||||||
@ -248,6 +248,8 @@ export type AgentCompactionConfig = {
|
|||||||
maxHistoryShare?: number;
|
maxHistoryShare?: number;
|
||||||
/** Pre-compaction memory flush (agentic turn). Default: enabled. */
|
/** Pre-compaction memory flush (agentic turn). Default: enabled. */
|
||||||
memoryFlush?: AgentCompactionMemoryFlushConfig;
|
memoryFlush?: AgentCompactionMemoryFlushConfig;
|
||||||
|
/** Post-compaction recovery turn (agentic turn). Default: enabled. */
|
||||||
|
postCompaction?: AgentCompactionPostCompactionConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AgentCompactionMemoryFlushConfig = {
|
export type AgentCompactionMemoryFlushConfig = {
|
||||||
@ -260,3 +262,12 @@ export type AgentCompactionMemoryFlushConfig = {
|
|||||||
/** System prompt appended for the memory flush turn. */
|
/** System prompt appended for the memory flush turn. */
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AgentCompactionPostCompactionConfig = {
|
||||||
|
/** Enable the post-compaction recovery turn (default: true). */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** User prompt used for the post-compaction recovery turn (NO_REPLY is enforced if missing). */
|
||||||
|
prompt?: string;
|
||||||
|
/** System prompt appended for the post-compaction recovery turn. */
|
||||||
|
systemPrompt?: string;
|
||||||
|
};
|
||||||
|
|||||||
@ -100,6 +100,14 @@ export const AgentDefaultsSchema = z
|
|||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
postCompaction: z
|
||||||
|
.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
prompt: z.string().optional(),
|
||||||
|
systemPrompt: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user