fix(auto-reply): move volatile inbound flags out of system metadata

Co-authored-by: aidiffuser <aidiffuser@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-02-23 19:05:57 +00:00
parent cf38339f25
commit 31e4c21b67
3 changed files with 49 additions and 11 deletions

View File

@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
- Agents/Overflow: add Chinese context-overflow pattern detection in `isContextOverflowError` so localized provider errors route through overflow recovery paths. (#22855) Thanks @Clawborn.
- Agents/Failover: treat HTTP 502/503/504 errors as failover-eligible transient timeouts so fallback chains can switch providers/models during upstream outages instead of retrying the same failing target. (#20999) Thanks @taw0002 and @vincentkoc.
- Auto-reply/Inbound metadata: hide direct-chat `message_id`/`message_id_full` and sender metadata only from normalized chat type (not sender-id sentinels), preserving group metadata visibility and preventing sender-id spoofed direct-mode classification. (#24373) thanks @jd316.
- Auto-reply/Inbound metadata: move dynamic inbound `flags` (reply/forward/thread/history) from system metadata to user-context conversation info, preventing turn-by-turn prompt-cache invalidation from flag toggles. (#21785) Thanks @aidiffuser.
- Auto-reply/Sessions: remove auth-key labels from `/new` and `/reset` confirmation messages so session reset notices never expose API key prefixes or env-key labels in chat output. (#24384, #24409) Thanks @Clawborn.
- Slack/Group policy: move Slack account `groupPolicy` defaulting to provider-level schema defaults so multi-account configs inherit top-level `channels.slack.groupPolicy` instead of silently overriding inheritance with per-account `allowlist`. (#17579) Thanks @ZetiMente.
- Providers/Anthropic: skip `context-1m-*` beta injection for OAuth/subscription tokens (`sk-ant-oat-*`) while preserving OAuth-required betas, avoiding Anthropic 401 auth failures when `params.context1m` is enabled. (#10647, #20354) Thanks @ClumsyWizardHands and @dcruver.

View File

@@ -57,6 +57,24 @@ describe("buildInboundMetaSystemPrompt", () => {
expect(payload["sender_id"]).toBeUndefined();
});
it("does not include per-turn flags in system metadata", () => {
const prompt = buildInboundMetaSystemPrompt({
ReplyToBody: "quoted",
ForwardedFrom: "sender",
ThreadStarterBody: "starter",
InboundHistory: [{ sender: "a", body: "b", timestamp: 1 }],
WasMentioned: true,
OriginatingTo: "telegram:-1001249586642",
OriginatingChannel: "telegram",
Provider: "telegram",
Surface: "telegram",
ChatType: "group",
} as TemplateContext);
const payload = parseInboundMetaPayload(prompt);
expect(payload["flags"]).toBeUndefined();
});
it("omits sender_id when blank", () => {
const prompt = buildInboundMetaSystemPrompt({
MessageSid: "458",
@@ -183,6 +201,25 @@ describe("buildInboundUserContextPrefix", () => {
expect(conversationInfo["sender_id"]).toBe("289522496");
});
it("includes dynamic per-turn flags in conversation info", () => {
const text = buildInboundUserContextPrefix({
ChatType: "group",
WasMentioned: true,
ReplyToBody: "quoted",
ForwardedFrom: "sender",
ThreadStarterBody: "starter",
InboundHistory: [{ sender: "a", body: "b", timestamp: 1 }],
} as TemplateContext);
const conversationInfo = parseConversationInfoPayload(text);
expect(conversationInfo["is_group_chat"]).toBe(true);
expect(conversationInfo["was_mentioned"]).toBe(true);
expect(conversationInfo["has_reply_context"]).toBe(true);
expect(conversationInfo["has_forwarded_context"]).toBe(true);
expect(conversationInfo["has_thread_starter"]).toBe(true);
expect(conversationInfo["history_count"]).toBe(1);
});
it("trims sender_id in conversation info", () => {
const text = buildInboundUserContextPrefix({
ChatType: "group",

View File

@@ -16,9 +16,9 @@ export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string {
// 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.
// Per-message identifiers and dynamic flags are also excluded here: they change on turns/replies
// and would bust prefix-based prompt caches on providers that use stable system prefixes.
// They are included in the user-role conversation info block instead.
// Resolve channel identity: prefer explicit channel, then surface, then provider.
// For webchat/Hub Chat sessions (when Surface is 'webchat' or undefined with no real channel),
@@ -43,14 +43,6 @@ export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string {
provider: safeTrim(ctx.Provider),
surface: safeTrim(ctx.Surface),
chat_type: chatType ?? (isDirect ? "direct" : undefined),
flags: {
is_group_chat: !isDirect ? true : undefined,
was_mentioned: ctx.WasMentioned === true ? true : undefined,
has_reply_context: Boolean(ctx.ReplyToBody),
has_forwarded_context: Boolean(ctx.ForwardedFrom),
has_thread_starter: Boolean(safeTrim(ctx.ThreadStarterBody)),
history_count: Array.isArray(ctx.InboundHistory) ? ctx.InboundHistory.length : 0,
},
};
// Keep the instructions local to the payload so the meaning survives prompt overrides.
@@ -92,7 +84,15 @@ export function buildInboundUserContextPrefix(ctx: TemplateContext): string {
group_space: safeTrim(ctx.GroupSpace),
thread_label: safeTrim(ctx.ThreadLabel),
is_forum: ctx.IsForum === true ? true : undefined,
is_group_chat: !isDirect ? true : undefined,
was_mentioned: ctx.WasMentioned === true ? true : undefined,
has_reply_context: ctx.ReplyToBody ? true : undefined,
has_forwarded_context: ctx.ForwardedFrom ? true : undefined,
has_thread_starter: safeTrim(ctx.ThreadStarterBody) ? true : undefined,
history_count:
Array.isArray(ctx.InboundHistory) && ctx.InboundHistory.length > 0
? ctx.InboundHistory.length
: undefined,
};
if (Object.values(conversationInfo).some((v) => v !== undefined)) {
blocks.push(