mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
d7627d6f4c
* refactor(prompt): plain inbound context labels with a provenance marker
Replaces trust-worded inbound context labels ("(untrusted metadata)",
"(untrusted, for context)") with plain labels plus a fixed provenance
marker suffix appended to every OpenClaw-injected context header.
Detection keys on the marker, not label text, so strippers stay correct
across UI, TUI, replay, /trace segmentation, memory recall, and the Swift
chat preprocessor. Drops sanitizeInboundSystemTags in favor of the marker
boundary plus trusted system-prompt narration.
Renames the untrusted-named plugin SDK context identifiers to
channel-provenance names, keeping deprecated aliases registered for
removal after 2026-09-08.
Adds `openclaw doctor --fix` migrations that rewrite legacy inbound
labels in stored SQLite transcripts and purge legacy envelope-
contaminated LanceDB recall rows.
* fix(ci): resolve gate failures for plain inbound context labels
- doctor sqlite readers: open read-only connections via openNodeSqliteDatabase
so the Kysely connection-boundary guardrail holds; unexport the now-internal
transcript snapshot type (Knip unused-export gate).
- compat registry: split the record table into registry-records.ts and
plugin-sdk-subpath-records.ts. The new compat record pushed registry.ts past
the 700-line oxlint cap; suppressions are disallowed, so follow the existing
sibling record-module pattern. Public exports and PluginCompatCode literals
unchanged.
- acp-runtime test: assert current finalization behavior (newline normalization
only). The bracket de-fang and System: rewrite it expected were removed with
sanitizeInboundSystemTags; forged system lines are neutralized at the
system-event queue, the single chokepoint feeding the System:-per-line render.
- regenerate docs_map and the plugin SDK API baseline manifest.
* fix(prompt): harden inbound context label migration and drop in-band sanitizer
Review follow-ups on the plain-label + provenance-marker change:
- Remove src/security/system-tags.ts. Rewriting inbound text to neutralize
look-alike `System:`/`[System]` markers corrupted legitimate user text and is
not a real injection boundary; role separation plus external-content wrapping
is. Explicit product decision, recorded at the system-event queue.
- Narrow the LanceDB legacy-row purge so it cannot delete benign memories. It
now requires a complete known legacy sentinel line, a legacy label followed by
a fenced JSON body, or the complete legacy external-content header. The prior
predicates matched ordinary prose such as `Notes (untrusted metadata):`, and
deletion is irreversible.
- Make explicit-empty canonical ChannelStructuredContext win over the deprecated
alias via a present/absent result instead of collapsing `[]` to undefined.
- Keep `\r?` in the active-memory doctor rule. It is the only rule spanning the
header's line break, migrated assistant rows skip newline normalization, and
without it the marked-header replace wins and the body strips to empty. Added
a CRLF regression test.
- Fix stale comments that described removed behavior, and cover the Swift
prose-block strip path.
Claude-Session: https://claude.ai/code/session_01WNzsPddQmxy9Y7jKD4wAxH
422 lines
15 KiB
TypeScript
422 lines
15 KiB
TypeScript
// Telegram tests cover bot message context.require mention plugin behavior.
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
const { buildTelegramMessageContextForTest } =
|
|
await import("./bot-message-context.test-harness.js");
|
|
const { buildTelegramSelfSenderName } = await import("./group-history-window.js");
|
|
|
|
describe("buildTelegramMessageContext requireMention precedence", () => {
|
|
function buildForumMessage(threadId = 99) {
|
|
return {
|
|
message_id: 1,
|
|
chat: {
|
|
id: -1001234567890,
|
|
type: "supergroup" as const,
|
|
title: "Forum",
|
|
is_forum: true,
|
|
},
|
|
date: 1_700_000_000,
|
|
text: "hello everyone",
|
|
message_thread_id: threadId,
|
|
from: { id: 42, first_name: "Alice" },
|
|
};
|
|
}
|
|
|
|
it("lets explicit topic requireMention=false override group requireMention=true", async () => {
|
|
const ctx = await buildTelegramMessageContextForTest({
|
|
message: buildForumMessage(),
|
|
resolveGroupActivation: () => undefined,
|
|
resolveGroupRequireMention: () => true,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: true },
|
|
topicConfig: { requireMention: false },
|
|
}),
|
|
});
|
|
|
|
if (!ctx) {
|
|
throw new Error("expected Telegram context when topic disables requireMention");
|
|
}
|
|
});
|
|
|
|
it("keeps unmentioned always-on group messages as user requests by default", async () => {
|
|
const ctx = await buildTelegramMessageContextForTest({
|
|
message: buildForumMessage(),
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
expect(ctx?.ctxPayload.InboundEventKind).toBe("user_request");
|
|
});
|
|
|
|
it("marks unmentioned always-on group messages as room events when configured", async () => {
|
|
const ctx = await buildTelegramMessageContextForTest({
|
|
cfg: { messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } } },
|
|
message: buildForumMessage(),
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
expect(ctx?.ctxPayload.InboundEventKind).toBe("room_event");
|
|
});
|
|
|
|
it("keeps explicit bot mentions as user requests in always-on room-event groups", async () => {
|
|
const ctx = await buildTelegramMessageContextForTest({
|
|
cfg: { messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } } },
|
|
message: {
|
|
...buildForumMessage(),
|
|
text: "@bot status",
|
|
entities: [{ type: "mention", offset: 0, length: "@bot".length }],
|
|
},
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
expect(ctx?.ctxPayload.InboundEventKind).toBe("user_request");
|
|
expect(ctx?.ctxPayload.WasMentioned).toBe(true);
|
|
expect(ctx?.ctxPayload.ExplicitlyMentionedBot).toBe(true);
|
|
});
|
|
|
|
it("keeps ambient abort phrases as user requests", async () => {
|
|
const ctx = await buildTelegramMessageContextForTest({
|
|
cfg: { messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } } },
|
|
message: { ...buildForumMessage(), text: "stop" },
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
expect(ctx?.ctxPayload.InboundEventKind).toBe("user_request");
|
|
});
|
|
|
|
it("keeps room events as context for the next direct group request", async () => {
|
|
const groupHistories = new Map();
|
|
const cfg = {
|
|
messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } },
|
|
};
|
|
await buildTelegramMessageContextForTest({
|
|
cfg,
|
|
message: { ...buildForumMessage(99), text: "side chatter" },
|
|
historyLimit: 10,
|
|
groupHistories,
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
const ctx = await buildTelegramMessageContextForTest({
|
|
cfg,
|
|
message: {
|
|
...buildForumMessage(99),
|
|
message_id: 2,
|
|
text: "replying directly",
|
|
reply_to_message: {
|
|
message_id: 10,
|
|
chat: { id: -1001234567890, type: "supergroup", title: "Forum", is_forum: true },
|
|
from: { id: 7, first_name: "Bot", username: "bot", is_bot: true },
|
|
text: "previous bot message",
|
|
},
|
|
},
|
|
historyLimit: 10,
|
|
groupHistories,
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
expect(ctx?.ctxPayload.InboundEventKind).toBe("user_request");
|
|
expect(JSON.stringify(ctx?.ctxPayload.ChannelStructuredContext)).toContain("side chatter");
|
|
expect(ctx?.ctxPayload.Body).not.toContain("side chatter");
|
|
});
|
|
|
|
it("keeps room events as context with default group history mode", async () => {
|
|
const groupHistories = new Map();
|
|
const cfg = {
|
|
messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } },
|
|
};
|
|
await buildTelegramMessageContextForTest({
|
|
cfg,
|
|
message: { ...buildForumMessage(99), text: "side chatter" },
|
|
historyLimit: 10,
|
|
groupHistories,
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
const ctx = await buildTelegramMessageContextForTest({
|
|
cfg,
|
|
message: {
|
|
...buildForumMessage(99),
|
|
message_id: 2,
|
|
text: "replying directly",
|
|
reply_to_message: {
|
|
message_id: 10,
|
|
chat: { id: -1001234567890, type: "supergroup", title: "Forum", is_forum: true },
|
|
from: { id: 7, first_name: "Bot", username: "bot", is_bot: true },
|
|
text: "previous bot message",
|
|
},
|
|
},
|
|
historyLimit: 10,
|
|
groupHistories,
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
expect(ctx?.ctxPayload.InboundEventKind).toBe("user_request");
|
|
expect(JSON.stringify(ctx?.ctxPayload.ChannelStructuredContext)).toContain("side chatter");
|
|
expect(ctx?.ctxPayload.Body).not.toContain("side chatter");
|
|
expect(ctx?.ctxPayload.InboundHistory).toEqual([
|
|
expect.objectContaining({ body: "side chatter" }),
|
|
]);
|
|
});
|
|
|
|
it("passes prior silent room events to the next default ambient turn", async () => {
|
|
const groupHistories = new Map();
|
|
const cfg = {
|
|
messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } },
|
|
};
|
|
await buildTelegramMessageContextForTest({
|
|
cfg,
|
|
message: { ...buildForumMessage(99), text: "Tell Sam deploy moved" },
|
|
historyLimit: 10,
|
|
groupHistories,
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
const ctx = await buildTelegramMessageContextForTest({
|
|
cfg,
|
|
message: { ...buildForumMessage(99), message_id: 2, text: "What changed?" },
|
|
historyLimit: 10,
|
|
groupHistories,
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
expect(ctx?.ctxPayload.InboundEventKind).toBe("room_event");
|
|
expect(ctx?.ctxPayload.InboundHistory).toEqual([
|
|
expect.objectContaining({ body: "Tell Sam deploy moved" }),
|
|
]);
|
|
});
|
|
|
|
it("passes user requests to later default ambient turns", async () => {
|
|
const groupHistories = new Map();
|
|
const cfg = {
|
|
messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } },
|
|
};
|
|
await buildTelegramMessageContextForTest({
|
|
cfg,
|
|
message: {
|
|
...buildForumMessage(99),
|
|
text: "@bot note the deploy moved",
|
|
entities: [{ type: "mention", offset: 0, length: 4 }],
|
|
},
|
|
historyLimit: 10,
|
|
groupHistories,
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
const ctx = await buildTelegramMessageContextForTest({
|
|
cfg,
|
|
message: { ...buildForumMessage(99), message_id: 2, text: "What now?" },
|
|
historyLimit: 10,
|
|
groupHistories,
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
expect(ctx?.ctxPayload.InboundEventKind).toBe("room_event");
|
|
expect(ctx?.ctxPayload.InboundHistory).toEqual([
|
|
expect.objectContaining({ body: "@bot note the deploy moved" }),
|
|
]);
|
|
});
|
|
|
|
it("uses outbound self entries as the non-destructive user-request watermark", async () => {
|
|
const historyKey = "-1001234567890:topic:99";
|
|
const groupHistories = new Map([
|
|
[
|
|
historyKey,
|
|
[
|
|
{ sender: "Alice", body: "before self marker", timestamp: 1, messageId: "1" },
|
|
{
|
|
sender: buildTelegramSelfSenderName("OpenClaw"),
|
|
body: "self marker body",
|
|
timestamp: 2,
|
|
messageId: "2",
|
|
},
|
|
{ sender: "Riley", body: "after watermark", timestamp: 3, messageId: "3" },
|
|
],
|
|
],
|
|
]);
|
|
const cfg = {
|
|
messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } },
|
|
};
|
|
|
|
const userRequest = await buildTelegramMessageContextForTest({
|
|
cfg,
|
|
message: {
|
|
...buildForumMessage(99),
|
|
message_id: 4,
|
|
text: "@bot answer after watermark",
|
|
entities: [{ type: "mention", offset: 0, length: 4 }],
|
|
},
|
|
historyLimit: 10,
|
|
groupHistories,
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
expect(userRequest?.ctxPayload.InboundEventKind).toBe("user_request");
|
|
expect(JSON.stringify(userRequest?.ctxPayload.ChannelStructuredContext)).toContain(
|
|
"after watermark",
|
|
);
|
|
expect(JSON.stringify(userRequest?.ctxPayload.ChannelStructuredContext)).not.toContain(
|
|
"before self marker",
|
|
);
|
|
expect(JSON.stringify(userRequest?.ctxPayload.ChannelStructuredContext)).not.toContain(
|
|
"self marker body",
|
|
);
|
|
expect(userRequest?.ctxPayload.Body).not.toContain("before self marker");
|
|
expect(userRequest?.ctxPayload.Body).not.toContain("self marker body");
|
|
expect(userRequest?.ctxPayload.InboundHistory).toEqual([
|
|
expect.objectContaining({ body: "after watermark" }),
|
|
]);
|
|
|
|
const roomEvent = await buildTelegramMessageContextForTest({
|
|
cfg,
|
|
message: { ...buildForumMessage(99), message_id: 5, text: "ambient after watermark" },
|
|
historyLimit: 10,
|
|
groupHistories,
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: undefined,
|
|
}),
|
|
});
|
|
|
|
expect(roomEvent?.ctxPayload.InboundEventKind).toBe("room_event");
|
|
expect(JSON.stringify(roomEvent?.ctxPayload.ChannelStructuredContext)).toContain(
|
|
"before self marker",
|
|
);
|
|
expect(JSON.stringify(roomEvent?.ctxPayload.ChannelStructuredContext)).toContain(
|
|
"self marker body",
|
|
);
|
|
expect(JSON.stringify(roomEvent?.ctxPayload.ChannelStructuredContext)).toContain(
|
|
"after watermark",
|
|
);
|
|
expect(roomEvent?.ctxPayload.Body).not.toContain("before self marker");
|
|
expect(roomEvent?.ctxPayload.InboundHistory).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ body: "before self marker" }),
|
|
expect.objectContaining({ body: "self marker body", sender: "OpenClaw (you)" }),
|
|
expect.objectContaining({ body: "after watermark" }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("lets explicit topic requireMention=false override mention activation", async () => {
|
|
const resolveGroupActivation = vi.fn(() => true);
|
|
|
|
const ctx = await buildTelegramMessageContextForTest({
|
|
message: buildForumMessage(),
|
|
resolveGroupActivation,
|
|
resolveGroupRequireMention: () => true,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: true },
|
|
topicConfig: { requireMention: false },
|
|
}),
|
|
});
|
|
|
|
if (!ctx?.ctxPayload) {
|
|
throw new Error("expected Telegram context payload when topic disables requireMention");
|
|
}
|
|
const activationCalls = resolveGroupActivation.mock.calls as unknown as Array<
|
|
[{ chatId: number; messageThreadId?: number; sessionKey: string }]
|
|
>;
|
|
const [activationOptions] = activationCalls[0] ?? [];
|
|
expect(activationOptions?.chatId).toBe(-1001234567890);
|
|
expect(activationOptions?.messageThreadId).toBe(99);
|
|
expect(activationOptions?.sessionKey).toBe("agent:main:telegram:group:-1001234567890:topic:99");
|
|
});
|
|
|
|
it("lets explicit topic requireMention=true override always activation", async () => {
|
|
const ctx = await buildTelegramMessageContextForTest({
|
|
message: buildForumMessage(),
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => false,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: false },
|
|
topicConfig: { requireMention: true },
|
|
}),
|
|
});
|
|
|
|
expect(ctx).toBeNull();
|
|
});
|
|
|
|
it("keeps activation fallback when no topic requireMention is configured", async () => {
|
|
const ctx = await buildTelegramMessageContextForTest({
|
|
message: buildForumMessage(),
|
|
resolveGroupActivation: () => false,
|
|
resolveGroupRequireMention: () => true,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig: { requireMention: true },
|
|
topicConfig: { agentId: "main" },
|
|
}),
|
|
});
|
|
|
|
if (!ctx) {
|
|
throw new Error("expected Telegram context when topic config keeps agent");
|
|
}
|
|
});
|
|
});
|