Agent: persist bootstrap warning dedupe history across runners

This commit is contained in:
Gustavo Madeira Santana
2026-03-03 10:35:16 -05:00
parent f49ccd054e
commit 7578a4e040
7 changed files with 139 additions and 17 deletions

View File

@@ -6,6 +6,7 @@ import {
buildBootstrapTruncationReportMeta,
buildBootstrapTruncationSignature,
formatBootstrapTruncationWarningLines,
resolveBootstrapWarningSignaturesSeen,
} from "./bootstrap-budget.js";
import type { WorkspaceBootstrapFile } from "./workspace.js";
@@ -103,6 +104,27 @@ describe("analyzeBootstrapBudget", () => {
});
describe("bootstrap prompt warnings", () => {
it("resolves seen signatures from report history or legacy single signature", () => {
expect(
resolveBootstrapWarningSignaturesSeen({
bootstrapTruncation: {
warningSignaturesSeen: ["sig-a", " ", "sig-b", "sig-a"],
promptWarningSignature: "legacy-ignored",
},
}),
).toEqual(["sig-a", "sig-b"]);
expect(
resolveBootstrapWarningSignaturesSeen({
bootstrapTruncation: {
promptWarningSignature: "legacy-only",
},
}),
).toEqual(["legacy-only"]);
expect(resolveBootstrapWarningSignaturesSeen(undefined)).toEqual([]);
});
it("dedupes warnings in once mode by signature", () => {
const analysis = analyzeBootstrapBudget({
files: [

View File

@@ -98,6 +98,24 @@ function appendSeenSignature(signatures: string[], signature: string): string[]
return next.slice(-DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX);
}
export function resolveBootstrapWarningSignaturesSeen(report?: {
bootstrapTruncation?: {
warningSignaturesSeen?: string[];
promptWarningSignature?: string;
};
}): string[] {
const truncation = report?.bootstrapTruncation;
const seenFromReport = normalizeSeenSignatures(truncation?.warningSignaturesSeen);
if (seenFromReport.length > 0) {
return seenFromReport;
}
const single =
typeof truncation?.promptWarningSignature === "string"
? truncation.promptWarningSignature.trim()
: "";
return single ? [single] : [];
}
export function buildBootstrapInjectionStats(params: {
bootstrapFiles: WorkspaceBootstrapFile[];
injectedFiles: EmbeddedContextFile[];

View File

@@ -1,5 +1,6 @@
import crypto from "node:crypto";
import fs from "node:fs";
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
import { runCliAgent } from "../../agents/cli-runner.js";
import { getCliSessionId } from "../../agents/cli-session.js";
import { runWithModelFallback } from "../../agents/model-fallback.js";
@@ -69,20 +70,6 @@ export type AgentRunLoopResult =
}
| { kind: "final"; payload: ReplyPayload };
function resolveBootstrapWarningSignaturesSeen(
report?: SessionEntry["systemPromptReport"],
): string[] {
const truncation = report?.bootstrapTruncation;
const seenFromReport = (truncation?.warningSignaturesSeen ?? []).filter(
(value): value is string => typeof value === "string" && value.trim().length > 0,
);
if (seenFromReport.length > 0) {
return Array.from(new Set(seenFromReport));
}
const single = truncation?.promptWarningSignature;
return typeof single === "string" && single.trim().length > 0 ? [single] : [];
}
export async function runAgentTurnWithFallback(params: {
commandBody: string;
followupRun: FollowupRun;

View File

@@ -14,6 +14,7 @@ import {
} from "../agents/agent-scope.js";
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
import { clearSessionAuthProfileOverride } from "../agents/auth-profiles/session-override.js";
import { resolveBootstrapWarningSignaturesSeen } from "../agents/bootstrap-budget.js";
import { runCliAgent } from "../agents/cli-runner.js";
import { getCliSessionId, setCliSessionId } from "../agents/cli-session.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
@@ -178,6 +179,11 @@ function runAgentAttempt(params: {
body: params.body,
isFallbackRetry: params.isFallbackRetry,
});
const bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
params.sessionEntry?.systemPromptReport,
);
const bootstrapPromptWarningSignature =
bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1];
if (isCliProvider(params.providerOverride, params.cfg)) {
const cliSessionId = getCliSessionId(params.sessionEntry, params.providerOverride);
const runCliWithSession = (nextCliSessionId: string | undefined) =>
@@ -196,6 +202,8 @@ function runAgentAttempt(params: {
runId: params.runId,
extraSystemPrompt: params.opts.extraSystemPrompt,
cliSessionId: nextCliSessionId,
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature,
images: params.isFallbackRetry ? undefined : params.opts.images,
streamParams: params.opts.streamParams,
});
@@ -317,6 +325,8 @@ function runAgentAttempt(params: {
streamParams: params.opts.streamParams,
agentDir: params.agentDir,
onAgentEvent: params.onAgentEvent,
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature,
});
}

View File

@@ -63,4 +63,65 @@ describe("updateSessionStoreAfterAgentRun", () => {
expect(persisted?.acp).toBeDefined();
expect(staleInMemory[sessionKey]?.acp).toBeDefined();
});
it("persists latest systemPromptReport for downstream warning dedupe", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-store-"));
const storePath = path.join(dir, "sessions.json");
const sessionKey = `agent:codex:report:${randomUUID()}`;
const sessionId = randomUUID();
const sessionStore: Record<string, SessionEntry> = {
[sessionKey]: {
sessionId,
updatedAt: Date.now(),
},
};
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf8");
const report = {
source: "run" as const,
generatedAt: Date.now(),
bootstrapTruncation: {
warningMode: "once" as const,
warningSignaturesSeen: ["sig-a", "sig-b"],
},
systemPrompt: {
chars: 1,
projectContextChars: 1,
nonProjectContextChars: 0,
},
injectedWorkspaceFiles: [],
skills: { promptChars: 0, entries: [] },
tools: { listChars: 0, schemaChars: 0, entries: [] },
};
await updateSessionStoreAfterAgentRun({
cfg: {} as never,
sessionId,
sessionKey,
storePath,
sessionStore,
defaultProvider: "openai",
defaultModel: "gpt-5.3-codex",
result: {
payloads: [],
meta: {
agentMeta: {
provider: "openai",
model: "gpt-5.3-codex",
},
systemPromptReport: report,
},
} as never,
});
const persisted = loadSessionStore(storePath, { skipCache: true })[sessionKey];
expect(persisted?.systemPromptReport?.bootstrapTruncation?.warningSignaturesSeen).toEqual([
"sig-a",
"sig-b",
]);
expect(sessionStore[sessionKey]?.systemPromptReport?.bootstrapTruncation?.warningMode).toBe(
"once",
);
});
});

View File

@@ -76,6 +76,9 @@ export async function updateSessionStoreAfterAgentRun(params: {
}
}
next.abortedLastRun = result.meta.aborted ?? false;
if (result.meta.systemPromptReport) {
next.systemPromptReport = result.meta.systemPromptReport;
}
if (hasNonzeroUsage(usage)) {
const input = usage.input ?? 0;
const output = usage.output ?? 0;

View File

@@ -6,6 +6,7 @@ import {
resolveDefaultAgentId,
} from "../../agents/agent-scope.js";
import { resolveSessionAuthProfileOverride } from "../../agents/auth-profiles/session-override.js";
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
import { runCliAgent } from "../../agents/cli-runner.js";
import { getCliSessionId, setCliSessionId } from "../../agents/cli-session.js";
import { lookupContextTokens } from "../../agents/context.js";
@@ -450,6 +451,9 @@ export async function runCronIsolatedAgentTurn(params: {
params.job.payload.kind === "agentTurn" && Array.isArray(params.job.payload.fallbacks)
? params.job.payload.fallbacks
: undefined;
let bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
cronSession.sessionEntry.systemPromptReport,
);
const fallbackResult = await runWithModelFallback({
cfg: cfgWithAgentDefaults,
provider,
@@ -457,10 +461,12 @@ export async function runCronIsolatedAgentTurn(params: {
agentDir,
fallbacksOverride:
payloadFallbacks ?? resolveAgentModelFallbacksOverride(params.cfg, agentId),
run: (providerOverride, modelOverride) => {
run: async (providerOverride, modelOverride) => {
if (abortSignal?.aborted) {
throw new Error(abortReason());
}
const bootstrapPromptWarningSignature =
bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1];
if (isCliProvider(providerOverride, cfgWithAgentDefaults)) {
// Fresh isolated cron sessions must not reuse a stored CLI session ID.
// Passing an existing ID activates the resume watchdog profile
@@ -470,7 +476,7 @@ export async function runCronIsolatedAgentTurn(params: {
const cliSessionId = cronSession.isNewSession
? undefined
: getCliSessionId(cronSession.sessionEntry, providerOverride);
return runCliAgent({
const result = await runCliAgent({
sessionId: cronSession.sessionEntry.sessionId,
sessionKey: agentSessionKey,
agentId,
@@ -484,9 +490,15 @@ export async function runCronIsolatedAgentTurn(params: {
timeoutMs,
runId: cronSession.sessionEntry.sessionId,
cliSessionId,
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature,
});
bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
result.meta?.systemPromptReport,
);
return result;
}
return runEmbeddedPiAgent({
const result = await runEmbeddedPiAgent({
sessionId: cronSession.sessionEntry.sessionId,
sessionKey: agentSessionKey,
agentId,
@@ -516,7 +528,13 @@ export async function runCronIsolatedAgentTurn(params: {
requireExplicitMessageTarget: deliveryRequested && resolvedDelivery.ok,
disableMessageTool: deliveryRequested || deliveryPlan.mode === "none",
abortSignal,
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature,
});
bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
result.meta?.systemPromptReport,
);
return result;
},
});
runResult = fallbackResult.result;
@@ -537,6 +555,9 @@ export async function runCronIsolatedAgentTurn(params: {
// Also collect best-effort telemetry for the cron run log.
let telemetry: CronRunTelemetry | undefined;
{
if (runResult.meta?.systemPromptReport) {
cronSession.sessionEntry.systemPromptReport = runResult.meta.systemPromptReport;
}
const usage = runResult.meta?.agentMeta?.usage;
const promptTokens = runResult.meta?.agentMeta?.promptTokens;
const modelUsed = runResult.meta?.agentMeta?.model ?? fallbackModel ?? model;