mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 14:04:31 +00:00
Agents: preserve bootstrap warning dedupe across followup runs
This commit is contained in:
@@ -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.
|
- 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/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.
|
- 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/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/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.
|
- 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 crypto from "node:crypto";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
|
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
|
||||||
import { estimateMessagesTokens } from "../../agents/compaction.js";
|
import { estimateMessagesTokens } from "../../agents/compaction.js";
|
||||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||||
import { isCliProvider } from "../../agents/model-selection.js";
|
import { isCliProvider } from "../../agents/model-selection.js";
|
||||||
@@ -452,6 +453,10 @@ export async function runMemoryFlushIfNeeded(params: {
|
|||||||
|
|
||||||
let activeSessionEntry = entry ?? params.sessionEntry;
|
let activeSessionEntry = entry ?? params.sessionEntry;
|
||||||
const activeSessionStore = params.sessionStore;
|
const activeSessionStore = params.sessionStore;
|
||||||
|
let bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
|
||||||
|
activeSessionEntry?.systemPromptReport ??
|
||||||
|
(params.sessionKey ? activeSessionStore?.[params.sessionKey]?.systemPromptReport : undefined),
|
||||||
|
);
|
||||||
const flushRunId = crypto.randomUUID();
|
const flushRunId = crypto.randomUUID();
|
||||||
if (params.sessionKey) {
|
if (params.sessionKey) {
|
||||||
registerAgentRunContext(flushRunId, {
|
registerAgentRunContext(flushRunId, {
|
||||||
@@ -469,7 +474,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
|||||||
try {
|
try {
|
||||||
await runWithModelFallback({
|
await runWithModelFallback({
|
||||||
...resolveModelFallbackOptions(params.followupRun.run),
|
...resolveModelFallbackOptions(params.followupRun.run),
|
||||||
run: (provider, model) => {
|
run: async (provider, model) => {
|
||||||
const { authProfile, embeddedContext, senderContext } = buildEmbeddedRunContexts({
|
const { authProfile, embeddedContext, senderContext } = buildEmbeddedRunContexts({
|
||||||
run: params.followupRun.run,
|
run: params.followupRun.run,
|
||||||
sessionCtx: params.sessionCtx,
|
sessionCtx: params.sessionCtx,
|
||||||
@@ -483,7 +488,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
|||||||
runId: flushRunId,
|
runId: flushRunId,
|
||||||
authProfile,
|
authProfile,
|
||||||
});
|
});
|
||||||
return runEmbeddedPiAgent({
|
const result = await runEmbeddedPiAgent({
|
||||||
...embeddedContext,
|
...embeddedContext,
|
||||||
...senderContext,
|
...senderContext,
|
||||||
...runBaseParams,
|
...runBaseParams,
|
||||||
@@ -493,6 +498,9 @@ export async function runMemoryFlushIfNeeded(params: {
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
}),
|
}),
|
||||||
extraSystemPrompt: flushSystemPrompt,
|
extraSystemPrompt: flushSystemPrompt,
|
||||||
|
bootstrapPromptWarningSignaturesSeen,
|
||||||
|
bootstrapPromptWarningSignature:
|
||||||
|
bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1],
|
||||||
onAgentEvent: (evt) => {
|
onAgentEvent: (evt) => {
|
||||||
if (evt.stream === "compaction") {
|
if (evt.stream === "compaction") {
|
||||||
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
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 =
|
let memoryFlushCompactionCount =
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ type AgentRunParams = {
|
|||||||
type EmbeddedRunParams = {
|
type EmbeddedRunParams = {
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
extraSystemPrompt?: string;
|
extraSystemPrompt?: string;
|
||||||
|
bootstrapPromptWarningSignaturesSeen?: string[];
|
||||||
|
bootstrapPromptWarningSignature?: string;
|
||||||
onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void;
|
onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1114,7 +1116,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
|||||||
const sessionId = "session";
|
const sessionId = "session";
|
||||||
const storePath = path.join(stateDir, "sessions", "sessions.json");
|
const storePath = path.join(stateDir, "sessions", "sessions.json");
|
||||||
const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId);
|
const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId);
|
||||||
const sessionEntry = {
|
const sessionEntry: SessionEntry = {
|
||||||
sessionId,
|
sessionId,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
sessionFile: transcriptPath,
|
sessionFile: transcriptPath,
|
||||||
@@ -1478,7 +1480,7 @@ describe("runReplyAgent memory flush", () => {
|
|||||||
it("skips memory flush for CLI providers", async () => {
|
it("skips memory flush for CLI providers", async () => {
|
||||||
await withTempStore(async (storePath) => {
|
await withTempStore(async (storePath) => {
|
||||||
const sessionKey = "main";
|
const sessionKey = "main";
|
||||||
const sessionEntry = {
|
const sessionEntry: SessionEntry = {
|
||||||
sessionId: "session",
|
sessionId: "session",
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
totalTokens: 80_000,
|
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 () => {
|
it("runs a memory flush turn and updates session metadata", async () => {
|
||||||
await withTempStore(async (storePath) => {
|
await withTempStore(async (storePath) => {
|
||||||
const sessionKey = "main";
|
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", () => {
|
describe("createFollowupRunner messaging tool dedupe", () => {
|
||||||
function createMessagingDedupeRunner(
|
function createMessagingDedupeRunner(
|
||||||
onBlockReply: (payload: unknown) => Promise<void>,
|
onBlockReply: (payload: unknown) => Promise<void>,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { resolveRunModelFallbacksOverride } from "../../agents/agent-scope.js";
|
import { resolveRunModelFallbacksOverride } from "../../agents/agent-scope.js";
|
||||||
|
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
|
||||||
import { lookupContextTokens } from "../../agents/context.js";
|
import { lookupContextTokens } from "../../agents/context.js";
|
||||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||||
@@ -140,6 +141,11 @@ export function createFollowupRunner(params: {
|
|||||||
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
||||||
let fallbackProvider = queued.run.provider;
|
let fallbackProvider = queued.run.provider;
|
||||||
let fallbackModel = queued.run.model;
|
let fallbackModel = queued.run.model;
|
||||||
|
const activeSessionEntry =
|
||||||
|
(sessionKey ? sessionStore?.[sessionKey] : undefined) ?? sessionEntry;
|
||||||
|
let bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
|
||||||
|
activeSessionEntry?.systemPromptReport,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const fallbackResult = await runWithModelFallback({
|
const fallbackResult = await runWithModelFallback({
|
||||||
cfg: queued.run.config,
|
cfg: queued.run.config,
|
||||||
@@ -151,9 +157,9 @@ export function createFollowupRunner(params: {
|
|||||||
agentId: queued.run.agentId,
|
agentId: queued.run.agentId,
|
||||||
sessionKey: queued.run.sessionKey,
|
sessionKey: queued.run.sessionKey,
|
||||||
}),
|
}),
|
||||||
run: (provider, model) => {
|
run: async (provider, model) => {
|
||||||
const authProfile = resolveRunAuthProfile(queued.run, provider);
|
const authProfile = resolveRunAuthProfile(queued.run, provider);
|
||||||
return runEmbeddedPiAgent({
|
const result = await runEmbeddedPiAgent({
|
||||||
sessionId: queued.run.sessionId,
|
sessionId: queued.run.sessionId,
|
||||||
sessionKey: queued.run.sessionKey,
|
sessionKey: queued.run.sessionKey,
|
||||||
agentId: queued.run.agentId,
|
agentId: queued.run.agentId,
|
||||||
@@ -195,6 +201,11 @@ export function createFollowupRunner(params: {
|
|||||||
timeoutMs: queued.run.timeoutMs,
|
timeoutMs: queued.run.timeoutMs,
|
||||||
runId,
|
runId,
|
||||||
blockReplyBreak: queued.run.blockReplyBreak,
|
blockReplyBreak: queued.run.blockReplyBreak,
|
||||||
|
bootstrapPromptWarningSignaturesSeen,
|
||||||
|
bootstrapPromptWarningSignature:
|
||||||
|
bootstrapPromptWarningSignaturesSeen[
|
||||||
|
bootstrapPromptWarningSignaturesSeen.length - 1
|
||||||
|
],
|
||||||
onAgentEvent: (evt) => {
|
onAgentEvent: (evt) => {
|
||||||
if (evt.stream !== "compaction") {
|
if (evt.stream !== "compaction") {
|
||||||
return;
|
return;
|
||||||
@@ -205,6 +216,10 @@ export function createFollowupRunner(params: {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
|
||||||
|
result.meta?.systemPromptReport,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
runResult = fallbackResult.result;
|
runResult = fallbackResult.result;
|
||||||
@@ -235,6 +250,7 @@ export function createFollowupRunner(params: {
|
|||||||
modelUsed,
|
modelUsed,
|
||||||
providerUsed: fallbackProvider,
|
providerUsed: fallbackProvider,
|
||||||
contextTokensUsed,
|
contextTokensUsed,
|
||||||
|
systemPromptReport: runResult.meta?.systemPromptReport,
|
||||||
logLabel: "followup",
|
logLabel: "followup",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user