Agents: preserve bootstrap warning dedupe across followup runs

This commit is contained in:
Gustavo Madeira Santana 2026-03-03 18:55:27 -05:00
parent d95cf256e7
commit 0d97101665
5 changed files with 172 additions and 6 deletions

View File

@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai
- iOS/TTS playback fallback: keep voice playback resilient by switching from PCM to MP3 when provider format support is unavailable, while avoiding sticky fallback on generic local playback errors. (#33032) thanks @mbelinky.
- Telegram/multi-account default routing clarity: warn only for ambiguous (2+) account setups without an explicit default, add `openclaw doctor` warnings for missing/invalid multi-account defaults across channels, and document explicit-default guidance for channel routing and Telegram config. (#32544) thanks @Sid-Qin.
- Telegram/plugin outbound hook parity: run `message_sending` + `message_sent` in Telegram reply delivery, include reply-path hook metadata (`mediaUrls`, `threadId`), and report `message_sent.success=false` when hooks blank text and no outbound message is delivered. (#32649) Thanks @KimGLee.
- Agents/bootstrap truncation warning handling: unify bootstrap budget/truncation analysis across embedded + CLI runtime, `/context`, and `openclaw doctor`; add `agents.defaults.bootstrapPromptTruncationWarning` (`off|once|always`, default `once`) and persist warning-signature metadata so truncation warnings are consistent and deduped across turns. (#32769) Thanks @gumadeiras.
- Agents/Skills runtime loading: propagate run config into embedded attempt and compaction skill-entry loading so explicitly enabled bundled companion skills are discovered consistently when skill snapshots do not already provide resolved entries. Thanks @gumadeiras.
- Agents/Session startup date grounding: substitute `YYYY-MM-DD` placeholders in startup/post-compaction AGENTS context and append runtime current-time lines for `/new` and `/reset` prompts so daily-memory references resolve correctly. (#32381) Thanks @chengzhichao-xydt.
- Agents/Compaction continuity: expand staged-summary merge instructions to preserve active task status, batch progress, latest user request, and follow-up commitments so compaction handoffs retain in-flight work context. (#8903) thanks @joetomasone.

View File

@ -1,6 +1,7 @@
import crypto from "node:crypto";
import fs from "node:fs";
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
import { estimateMessagesTokens } from "../../agents/compaction.js";
import { runWithModelFallback } from "../../agents/model-fallback.js";
import { isCliProvider } from "../../agents/model-selection.js";
@ -452,6 +453,10 @@ export async function runMemoryFlushIfNeeded(params: {
let activeSessionEntry = entry ?? params.sessionEntry;
const activeSessionStore = params.sessionStore;
let bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
activeSessionEntry?.systemPromptReport ??
(params.sessionKey ? activeSessionStore?.[params.sessionKey]?.systemPromptReport : undefined),
);
const flushRunId = crypto.randomUUID();
if (params.sessionKey) {
registerAgentRunContext(flushRunId, {
@ -469,7 +474,7 @@ export async function runMemoryFlushIfNeeded(params: {
try {
await runWithModelFallback({
...resolveModelFallbackOptions(params.followupRun.run),
run: (provider, model) => {
run: async (provider, model) => {
const { authProfile, embeddedContext, senderContext } = buildEmbeddedRunContexts({
run: params.followupRun.run,
sessionCtx: params.sessionCtx,
@ -483,7 +488,7 @@ export async function runMemoryFlushIfNeeded(params: {
runId: flushRunId,
authProfile,
});
return runEmbeddedPiAgent({
const result = await runEmbeddedPiAgent({
...embeddedContext,
...senderContext,
...runBaseParams,
@ -493,6 +498,9 @@ export async function runMemoryFlushIfNeeded(params: {
cfg: params.cfg,
}),
extraSystemPrompt: flushSystemPrompt,
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature:
bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1],
onAgentEvent: (evt) => {
if (evt.stream === "compaction") {
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
@ -502,6 +510,10 @@ export async function runMemoryFlushIfNeeded(params: {
}
},
});
bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
result.meta?.systemPromptReport,
);
return result;
},
});
let memoryFlushCompactionCount =

View File

@ -28,6 +28,8 @@ type AgentRunParams = {
type EmbeddedRunParams = {
prompt?: string;
extraSystemPrompt?: string;
bootstrapPromptWarningSignaturesSeen?: string[];
bootstrapPromptWarningSignature?: string;
onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void;
};
@ -1114,7 +1116,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
const sessionId = "session";
const storePath = path.join(stateDir, "sessions", "sessions.json");
const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId);
const sessionEntry = {
const sessionEntry: SessionEntry = {
sessionId,
updatedAt: Date.now(),
sessionFile: transcriptPath,
@ -1478,7 +1480,7 @@ describe("runReplyAgent memory flush", () => {
it("skips memory flush for CLI providers", async () => {
await withTempStore(async (storePath) => {
const sessionKey = "main";
const sessionEntry = {
const sessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 80_000,
@ -1577,6 +1579,77 @@ describe("runReplyAgent memory flush", () => {
});
});
it("passes stored bootstrap warning signatures to memory flush runs", async () => {
await withTempStore(async (storePath) => {
const sessionKey = "main";
const sessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 80_000,
compactionCount: 1,
systemPromptReport: {
source: "run",
generatedAt: Date.now(),
systemPrompt: {
chars: 1,
projectContextChars: 0,
nonProjectContextChars: 1,
},
injectedWorkspaceFiles: [],
skills: {
promptChars: 0,
entries: [],
},
tools: {
listChars: 0,
schemaChars: 0,
entries: [],
},
bootstrapTruncation: {
warningMode: "once",
warningShown: true,
promptWarningSignature: "sig-b",
warningSignaturesSeen: ["sig-a", "sig-b"],
truncatedFiles: 1,
nearLimitFiles: 0,
totalNearLimit: false,
},
},
};
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
const calls: Array<EmbeddedRunParams> = [];
state.runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => {
calls.push(params);
if (params.prompt?.includes("Pre-compaction memory flush.")) {
return { payloads: [], meta: {} };
}
return {
payloads: [{ text: "ok" }],
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
};
});
const baseRun = createBaseRun({
storePath,
sessionEntry,
});
await runReplyAgentWithBase({
baseRun,
storePath,
sessionKey,
sessionEntry,
commandBody: "hello",
});
expect(calls).toHaveLength(2);
expect(calls[0]?.bootstrapPromptWarningSignaturesSeen).toEqual(["sig-a", "sig-b"]);
expect(calls[0]?.bootstrapPromptWarningSignature).toBe("sig-b");
});
});
it("runs a memory flush turn and updates session metadata", async () => {
await withTempStore(async (storePath) => {
const sessionKey = "main";

View File

@ -163,6 +163,70 @@ describe("createFollowupRunner compaction", () => {
});
});
describe("createFollowupRunner bootstrap warning dedupe", () => {
it("passes stored warning signature history to embedded followup runs", async () => {
runEmbeddedPiAgentMock.mockResolvedValueOnce({
payloads: [],
meta: {},
});
const sessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
systemPromptReport: {
source: "run",
generatedAt: Date.now(),
systemPrompt: {
chars: 1,
projectContextChars: 0,
nonProjectContextChars: 1,
},
injectedWorkspaceFiles: [],
skills: {
promptChars: 0,
entries: [],
},
tools: {
listChars: 0,
schemaChars: 0,
entries: [],
},
bootstrapTruncation: {
warningMode: "once",
warningShown: true,
promptWarningSignature: "sig-b",
warningSignaturesSeen: ["sig-a", "sig-b"],
truncatedFiles: 1,
nearLimitFiles: 0,
totalNearLimit: false,
},
},
};
const sessionStore: Record<string, SessionEntry> = { main: sessionEntry };
const runner = createFollowupRunner({
opts: { onBlockReply: vi.fn(async () => {}) },
typing: createMockTypingController(),
typingMode: "instant",
sessionEntry,
sessionStore,
sessionKey: "main",
defaultModel: "anthropic/claude-opus-4-5",
});
await runner(baseQueuedRun());
const call = runEmbeddedPiAgentMock.mock.calls.at(-1)?.[0] as
| {
bootstrapPromptWarningSignaturesSeen?: string[];
bootstrapPromptWarningSignature?: string;
}
| undefined;
expect(call?.bootstrapPromptWarningSignaturesSeen).toEqual(["sig-a", "sig-b"]);
expect(call?.bootstrapPromptWarningSignature).toBe("sig-b");
});
});
describe("createFollowupRunner messaging tool dedupe", () => {
function createMessagingDedupeRunner(
onBlockReply: (payload: unknown) => Promise<void>,

View File

@ -1,5 +1,6 @@
import crypto from "node:crypto";
import { resolveRunModelFallbacksOverride } from "../../agents/agent-scope.js";
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
import { lookupContextTokens } from "../../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
import { runWithModelFallback } from "../../agents/model-fallback.js";
@ -140,6 +141,11 @@ export function createFollowupRunner(params: {
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
let fallbackProvider = queued.run.provider;
let fallbackModel = queued.run.model;
const activeSessionEntry =
(sessionKey ? sessionStore?.[sessionKey] : undefined) ?? sessionEntry;
let bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
activeSessionEntry?.systemPromptReport,
);
try {
const fallbackResult = await runWithModelFallback({
cfg: queued.run.config,
@ -151,9 +157,9 @@ export function createFollowupRunner(params: {
agentId: queued.run.agentId,
sessionKey: queued.run.sessionKey,
}),
run: (provider, model) => {
run: async (provider, model) => {
const authProfile = resolveRunAuthProfile(queued.run, provider);
return runEmbeddedPiAgent({
const result = await runEmbeddedPiAgent({
sessionId: queued.run.sessionId,
sessionKey: queued.run.sessionKey,
agentId: queued.run.agentId,
@ -195,6 +201,11 @@ export function createFollowupRunner(params: {
timeoutMs: queued.run.timeoutMs,
runId,
blockReplyBreak: queued.run.blockReplyBreak,
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature:
bootstrapPromptWarningSignaturesSeen[
bootstrapPromptWarningSignaturesSeen.length - 1
],
onAgentEvent: (evt) => {
if (evt.stream !== "compaction") {
return;
@ -205,6 +216,10 @@ export function createFollowupRunner(params: {
}
},
});
bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
result.meta?.systemPromptReport,
);
return result;
},
});
runResult = fallbackResult.result;
@ -235,6 +250,7 @@ export function createFollowupRunner(params: {
modelUsed,
providerUsed: fallbackProvider,
contextTokensUsed,
systemPromptReport: runResult.meta?.systemPromptReport,
logLabel: "followup",
});
}