mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 03:07:28 +00:00
Agent: persist bootstrap warning dedupe history across runners
This commit is contained in:
@@ -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: [
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user