mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(auto-reply): restore prompt cache stability by moving per-turn ids to user context (#20597)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 175919afb6
Co-authored-by: anisoptera <768771+anisoptera@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
@@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/Daemon: forward `TMPDIR` into installed service environments so macOS LaunchAgent gateway runs can open SQLite temp/journal files reliably instead of failing with `SQLITE_CANTOPEN`. (#20512) Thanks @Clawborn.
|
||||
- Agents/Billing: include the active model that produced a billing error in user-facing billing messages (for example, `OpenAI (gpt-5.3)`) across payload, failover, and lifecycle error paths, so users can identify exactly which key needs credits. (#20510) Thanks @echoVic.
|
||||
- Gateway/TUI: honor `agents.defaults.blockStreamingDefault` for `chat.send` by removing the hardcoded block-streaming disable override, so replies can use configured block-mode delivery. (#19693) Thanks @neipor.
|
||||
- Auto-reply/Prompt caching: restore prefix-cache stability by keeping inbound system metadata session-stable and moving per-message IDs (`message_id`, `message_id_full`, `reply_to_id`, `sender_id`) into untrusted conversation context. (#20597) Thanks @anisoptera.
|
||||
- UI/Sessions: accept the canonical main session-key alias in Chat UI flows so main-session routing stays consistent. (#20311) Thanks @mbelinky.
|
||||
- OpenClawKit/Protocol: preserve JSON boolean literals (`true`/`false`) when bridging through `AnyCodable` so Apple client RPC params no longer re-encode booleans as `1`/`0`. Thanks @mbelinky.
|
||||
- Commands/Doctor: skip embedding-provider warnings when `memory.backend` is `qmd`, because QMD manages embeddings internally and does not require `memorySearch` providers. (#17263) Thanks @miloudbelarebia.
|
||||
|
||||
@@ -19,7 +19,7 @@ function parseConversationInfoPayload(text: string): Record<string, unknown> {
|
||||
}
|
||||
|
||||
describe("buildInboundMetaSystemPrompt", () => {
|
||||
it("includes trusted message and routing ids for tool actions", () => {
|
||||
it("includes session-stable routing fields", () => {
|
||||
const prompt = buildInboundMetaSystemPrompt({
|
||||
MessageSid: "123",
|
||||
MessageSidFull: "123",
|
||||
@@ -33,41 +33,28 @@ describe("buildInboundMetaSystemPrompt", () => {
|
||||
|
||||
const payload = parseInboundMetaPayload(prompt);
|
||||
expect(payload["schema"]).toBe("openclaw.inbound_meta.v1");
|
||||
expect(payload["message_id"]).toBe("123");
|
||||
expect(payload["message_id_full"]).toBeUndefined();
|
||||
expect(payload["reply_to_id"]).toBe("99");
|
||||
expect(payload["chat_id"]).toBe("telegram:5494292670");
|
||||
expect(payload["channel"]).toBe("telegram");
|
||||
});
|
||||
|
||||
it("includes sender_id when provided", () => {
|
||||
it("does not include per-turn message identifiers (cache stability)", () => {
|
||||
const prompt = buildInboundMetaSystemPrompt({
|
||||
MessageSid: "456",
|
||||
MessageSid: "123",
|
||||
MessageSidFull: "123",
|
||||
ReplyToId: "99",
|
||||
SenderId: "289522496",
|
||||
OriginatingTo: "telegram:-1001249586642",
|
||||
OriginatingTo: "telegram:5494292670",
|
||||
OriginatingChannel: "telegram",
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
ChatType: "group",
|
||||
ChatType: "direct",
|
||||
} as TemplateContext);
|
||||
|
||||
const payload = parseInboundMetaPayload(prompt);
|
||||
expect(payload["sender_id"]).toBe("289522496");
|
||||
});
|
||||
|
||||
it("trims sender_id before storing", () => {
|
||||
const prompt = buildInboundMetaSystemPrompt({
|
||||
MessageSid: "457",
|
||||
SenderId: " 289522496 ",
|
||||
OriginatingTo: "telegram:-1001249586642",
|
||||
OriginatingChannel: "telegram",
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
ChatType: "group",
|
||||
} as TemplateContext);
|
||||
|
||||
const payload = parseInboundMetaPayload(prompt);
|
||||
expect(payload["sender_id"]).toBe("289522496");
|
||||
expect(payload["message_id"]).toBeUndefined();
|
||||
expect(payload["message_id_full"]).toBeUndefined();
|
||||
expect(payload["reply_to_id"]).toBeUndefined();
|
||||
expect(payload["sender_id"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits sender_id when blank", () => {
|
||||
@@ -84,36 +71,6 @@ describe("buildInboundMetaSystemPrompt", () => {
|
||||
const payload = parseInboundMetaPayload(prompt);
|
||||
expect(payload["sender_id"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits sender_id when not provided", () => {
|
||||
const prompt = buildInboundMetaSystemPrompt({
|
||||
MessageSid: "789",
|
||||
OriginatingTo: "telegram:5494292670",
|
||||
OriginatingChannel: "telegram",
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
ChatType: "direct",
|
||||
} as TemplateContext);
|
||||
|
||||
const payload = parseInboundMetaPayload(prompt);
|
||||
expect(payload["sender_id"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps message_id_full only when it differs from message_id", () => {
|
||||
const prompt = buildInboundMetaSystemPrompt({
|
||||
MessageSid: "short-id",
|
||||
MessageSidFull: "full-provider-message-id",
|
||||
OriginatingTo: "channel:C1",
|
||||
OriginatingChannel: "slack",
|
||||
Provider: "slack",
|
||||
Surface: "slack",
|
||||
ChatType: "group",
|
||||
} as TemplateContext);
|
||||
|
||||
const payload = parseInboundMetaPayload(prompt);
|
||||
expect(payload["message_id"]).toBe("short-id");
|
||||
expect(payload["message_id_full"]).toBe("full-provider-message-id");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildInboundUserContextPrefix", () => {
|
||||
@@ -156,6 +113,63 @@ describe("buildInboundUserContextPrefix", () => {
|
||||
expect(conversationInfo["message_id"]).toBe("msg-123");
|
||||
});
|
||||
|
||||
it("includes message_id_full when it differs from message_id", () => {
|
||||
const text = buildInboundUserContextPrefix({
|
||||
ChatType: "group",
|
||||
MessageSid: "short-id",
|
||||
MessageSidFull: "full-provider-message-id",
|
||||
} as TemplateContext);
|
||||
|
||||
const conversationInfo = parseConversationInfoPayload(text);
|
||||
expect(conversationInfo["message_id"]).toBe("short-id");
|
||||
expect(conversationInfo["message_id_full"]).toBe("full-provider-message-id");
|
||||
});
|
||||
|
||||
it("omits message_id_full when it matches message_id", () => {
|
||||
const text = buildInboundUserContextPrefix({
|
||||
ChatType: "direct",
|
||||
MessageSid: "same-id",
|
||||
MessageSidFull: "same-id",
|
||||
} as TemplateContext);
|
||||
|
||||
const conversationInfo = parseConversationInfoPayload(text);
|
||||
expect(conversationInfo["message_id"]).toBe("same-id");
|
||||
expect(conversationInfo["message_id_full"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includes reply_to_id in conversation info", () => {
|
||||
const text = buildInboundUserContextPrefix({
|
||||
ChatType: "direct",
|
||||
MessageSid: "msg-200",
|
||||
ReplyToId: "msg-199",
|
||||
} as TemplateContext);
|
||||
|
||||
const conversationInfo = parseConversationInfoPayload(text);
|
||||
expect(conversationInfo["reply_to_id"]).toBe("msg-199");
|
||||
});
|
||||
|
||||
it("includes sender_id in conversation info", () => {
|
||||
const text = buildInboundUserContextPrefix({
|
||||
ChatType: "group",
|
||||
MessageSid: "msg-456",
|
||||
SenderId: "289522496",
|
||||
} as TemplateContext);
|
||||
|
||||
const conversationInfo = parseConversationInfoPayload(text);
|
||||
expect(conversationInfo["sender_id"]).toBe("289522496");
|
||||
});
|
||||
|
||||
it("trims sender_id in conversation info", () => {
|
||||
const text = buildInboundUserContextPrefix({
|
||||
ChatType: "direct",
|
||||
MessageSid: "msg-457",
|
||||
SenderId: " 289522496 ",
|
||||
} as TemplateContext);
|
||||
|
||||
const conversationInfo = parseConversationInfoPayload(text);
|
||||
expect(conversationInfo["sender_id"]).toBe("289522496");
|
||||
});
|
||||
|
||||
it("falls back to SenderId when sender phone is missing", () => {
|
||||
const text = buildInboundUserContextPrefix({
|
||||
ChatType: "direct",
|
||||
|
||||
@@ -13,20 +13,15 @@ function safeTrim(value: unknown): string | undefined {
|
||||
export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string {
|
||||
const chatType = normalizeChatType(ctx.ChatType);
|
||||
const isDirect = !chatType || chatType === "direct";
|
||||
const messageId = safeTrim(ctx.MessageSid);
|
||||
const messageIdFull = safeTrim(ctx.MessageSidFull);
|
||||
const replyToId = safeTrim(ctx.ReplyToId);
|
||||
const chatId = safeTrim(ctx.OriginatingTo);
|
||||
|
||||
// Keep system metadata strictly free of attacker-controlled strings (sender names, group subjects, etc.).
|
||||
// Those belong in the user-role "untrusted context" blocks.
|
||||
// Per-message identifiers (message_id, reply_to_id, sender_id) are also excluded here: they change
|
||||
// on every turn and would bust prefix-based prompt caches on local model providers. They are
|
||||
// included in the user-role conversation info block via buildInboundUserContextPrefix() instead.
|
||||
const payload = {
|
||||
schema: "openclaw.inbound_meta.v1",
|
||||
message_id: messageId,
|
||||
message_id_full: messageIdFull && messageIdFull !== messageId ? messageIdFull : undefined,
|
||||
sender_id: safeTrim(ctx.SenderId),
|
||||
chat_id: chatId,
|
||||
reply_to_id: replyToId,
|
||||
chat_id: safeTrim(ctx.OriginatingTo),
|
||||
channel: safeTrim(ctx.OriginatingChannel) ?? safeTrim(ctx.Surface) ?? safeTrim(ctx.Provider),
|
||||
provider: safeTrim(ctx.Provider),
|
||||
surface: safeTrim(ctx.Surface),
|
||||
@@ -60,8 +55,13 @@ export function buildInboundUserContextPrefix(ctx: TemplateContext): string {
|
||||
const chatType = normalizeChatType(ctx.ChatType);
|
||||
const isDirect = !chatType || chatType === "direct";
|
||||
|
||||
const messageId = safeTrim(ctx.MessageSid);
|
||||
const messageIdFull = safeTrim(ctx.MessageSidFull);
|
||||
const conversationInfo = {
|
||||
message_id: safeTrim(ctx.MessageSid),
|
||||
message_id: messageId,
|
||||
message_id_full: messageIdFull && messageIdFull !== messageId ? messageIdFull : undefined,
|
||||
reply_to_id: safeTrim(ctx.ReplyToId),
|
||||
sender_id: safeTrim(ctx.SenderId),
|
||||
conversation_label: isDirect ? undefined : safeTrim(ctx.ConversationLabel),
|
||||
sender: safeTrim(ctx.SenderE164) ?? safeTrim(ctx.SenderId) ?? safeTrim(ctx.SenderUsername),
|
||||
group_subject: safeTrim(ctx.GroupSubject),
|
||||
|
||||
Reference in New Issue
Block a user