mirror of https://github.com/openclaw/openclaw.git
Agents: preserve bootstrap warning dedupe across followup runs
This commit is contained in:
parent
d95cf256e7
commit
0d97101665
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue