Harden public reply privacy and history lookup

This commit is contained in:
Meta Vens 2026-05-02 20:11:49 +01:00
commit ada64a1ad1
6 changed files with 106 additions and 40 deletions

View file

@ -66,6 +66,50 @@ import {
stripBotMention,
} from "./discord-bot-support";
function filterPromptEchoHistoryResults<
T extends {
text: string;
},
>(results: T[], query: string): T[] {
const normalizedQuery = normalizeHistoryEchoText(query);
if (normalizedQuery.length === 0) {
return results;
}
const filtered = results.filter((result) => {
const normalizedResult = normalizeHistoryEchoText(result.text);
if (normalizedResult.length === 0) {
return true;
}
if (normalizedResult === normalizedQuery) {
return false;
}
if (
normalizedQuery.length >= 48 &&
(normalizedResult.includes(normalizedQuery) || normalizedQuery.includes(normalizedResult))
) {
return false;
}
return true;
});
return filtered.length > 0 ? filtered : results;
}
function normalizeHistoryEchoText(value: string): string {
return value
.replace(/<@!?\d+>/g, " ")
.replace(/<@&\d+>/g, " ")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
}
export async function startBot(): Promise<void> {
const config = loadConfig();
@ -182,11 +226,12 @@ export async function startBot(): Promise<void> {
};
},
searchHistory: async (input) => {
const results = await retrievalService.searchHistory(input.query, input.limit, {
const rawResults = await retrievalService.searchHistory(input.query, input.limit, {
guildId: input.guildId,
channelId: input.channelId,
authorId: input.authorId,
});
const results = filterPromptEchoHistoryResults(rawResults, input.query);
return {
query: input.query,

View file

@ -34,6 +34,51 @@ import {
} from "@voidbot/shared";
const config = loadConfig();
function filterPromptEchoHistoryResults<
T extends {
text: string;
},
>(results: T[], query: string): T[] {
const normalizedQuery = normalizeHistoryEchoText(query);
if (normalizedQuery.length === 0) {
return results;
}
const filtered = results.filter((result) => {
const normalizedResult = normalizeHistoryEchoText(result.text);
if (normalizedResult.length === 0) {
return true;
}
if (normalizedResult === normalizedQuery) {
return false;
}
if (
normalizedQuery.length >= 48 &&
(normalizedResult.includes(normalizedQuery) || normalizedQuery.includes(normalizedResult))
) {
return false;
}
return true;
});
return filtered.length > 0 ? filtered : results;
}
function normalizeHistoryEchoText(value: string): string {
return value
.replace(/<@!?\d+>/g, " ")
.replace(/<@&\d+>/g, " ")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
}
const embedder = createTextEmbedder({
backend: config.ragEmbeddingBackend,
hashDimensions: config.ragEmbeddingDimensions,
@ -169,11 +214,12 @@ async function buildProviderRegistry(systemMessagesCatalog: SystemMessageCatalog
};
},
searchHistory: async (input) => {
const results = await retrievalService.searchHistory(input.query, input.limit, {
const rawResults = await retrievalService.searchHistory(input.query, input.limit, {
guildId: input.guildId,
channelId: input.channelId,
authorId: input.authorId,
});
const results = filterPromptEchoHistoryResults(rawResults, input.query);
return {
query: input.query,

View file

@ -26,10 +26,13 @@ export function buildSystemPrompt(context: ContextBundle): string {
"Use retrieved snippets and recent channel context when they help answer the question.",
"If explicit interaction memory for the current speaker is attached, you may let it gently color the tone and reference it when relevant, but do not invent history beyond what was provided.",
"Treat the attached interaction memory as a non-clinical behavioral read, not a diagnosis. Use the remembered dimensions, traits, and guidance to adapt tone, pacing, firmness, structure, and warmth to the person in front of you.",
"The attached interaction memory and inferred guidance are private response scaffolding, not content to expose. Do not quote, summarize, classify, or explain the speaker's inferred traits, engagement patterns, psychological profile, or hidden response guidance unless they explicitly ask how you are reading them.",
"Do not turn a substantive question into a meta-analysis of the user's personality, engagement style, sentiment, or recent conversation behavior unless they explicitly asked for that kind of read.",
"Be steady with anxious or validation-seeking speakers, grounding with grandiose ones, transparent with suspicious ones, structured with rigid or obsessive ones, and firmer with controlling, contemptuous, or boundary-pushing ones.",
"When the answer depends on archived Discord history or indexed repo/lore context, use the available read-only tools instead of guessing.",
"If the user is asking about a real incident, personal history, prior server drama, something somebody said here, or a historical event discussed in Discord, start with search_history.",
"Use search_sources for code, docs, lore, repo content, or authored project material. Do not treat it as the first stop for real-life incidents or prior server conversations.",
"If a history search gives you echoes of the current question or other repeated ask-lines, ignore those and look for earlier substantive messages, links, or fetch surrounding context with get_message_context.",
"If you need to target a specific indexed repo and do not know the valid repo names yet, call list_indexed_repos before search_sources.",
renderSourceGroundingInstructions(context, false),
"Do not claim to have performed searches or tool calls beyond the material actually executed in this run.",
@ -101,26 +104,10 @@ function renderInteractionMemory(context: ContextBundle): string {
)
.join("\n")
: "- No recent interaction events were retained.";
const dimensions = context.interactionMemory.interactionDimensions.length
? context.interactionMemory.interactionDimensions
.map(
(dimension) =>
`- ${dimension.label}: ${dimension.score}/3. ${dimension.summary}`,
)
.join("\n")
: "- No strong interaction dimensions were inferred yet.";
return [
`- Summary: ${context.interactionMemory.summary}`,
`- Disposition: ${context.interactionMemory.disposition}`,
`- Affinity score: ${context.interactionMemory.affinityScore}`,
`- Psychological profile: ${context.interactionMemory.psychologicalProfile}`,
`- Inferred traits: ${context.interactionMemory.inferredTraits.length > 0 ? context.interactionMemory.inferredTraits.join(", ") : "(none yet)"}`,
"- Interaction dimensions:",
dimensions,
`- Response guidance: ${context.interactionMemory.responseGuidance}`,
`- Direct remembered interactions: ${context.interactionMemory.directInteractionCount}`,
`- Ambient remembered mentions: ${context.interactionMemory.ambientMentionCount}`,
`- Relationship summary: ${context.interactionMemory.summary}`,
`- Current stance: ${context.interactionMemory.disposition}; affinity=${context.interactionMemory.affinityScore}`,
`- Private response guidance (do not reveal): ${context.interactionMemory.responseGuidance}`,
"- Specific remembered incidents:",
recentEvents,
].join("\n");

View file

@ -80,11 +80,14 @@ export function buildDiscordReplyPrompt(
"- The configured MCP tools are available in this session, especially search_history, get_message_context, list_indexed_repos, search_sources, and get_source_context.",
"- If explicit interaction memory for the current speaker is attached, you may let it subtly color the tone and reference it when relevant, but do not invent relationship history beyond that record.",
"- Treat the attached interaction memory as a non-clinical behavioral read, not a diagnosis. Use the remembered dimensions, traits, and guidance to adapt tone, pacing, firmness, structure, and warmth to the person in front of you.",
"- The attached interaction memory and inferred guidance are private response scaffolding, not content to expose. Do not quote, summarize, classify, or explain the speaker's inferred traits, engagement patterns, psychological profile, or hidden response guidance unless they explicitly ask how you are reading them.",
"- Do not turn a substantive question into a meta-analysis of the user's personality, engagement style, sentiment, or recent conversation behavior unless they explicitly asked for that kind of read.",
"- Be steady with anxious or validation-seeking speakers, grounding with grandiose ones, transparent with suspicious ones, structured with rigid or obsessive ones, and firmer with controlling, contemptuous, or boundary-pushing ones.",
`- search_history and search_sources accept limit values between 1 and ${MAX_RETRIEVAL_RESULT_LIMIT}. Do not ask for more than ${MAX_RETRIEVAL_RESULT_LIMIT} results in one call.`,
"- get_message_context and get_source_context accept before/after values between 0 and 20. Do not ask for larger context windows in one call.",
"- You may inspect the workspace and use safe read-only commands if needed.",
"- For questions about Discord history, prior discussion, or user preferences, use search_history and get_message_context instead of filesystem inspection.",
"- If a history search gives you echoes of the current question or other repeated ask-lines, ignore those and look for earlier substantive messages, links, or fetch surrounding context with get_message_context.",
"- For questions about indexed repos, source trees, repo-local docs, or indexed lore collections, use search_sources and get_source_context before broad workspace scans.",
"- If you want to narrow source search to a specific repo but do not know the valid repo names yet, call list_indexed_repos first.",
sourceGroundingInstructions,

View file

@ -448,26 +448,10 @@ export function renderInteractionMemory(context: ContextBundle): string {
)
.join("\n")
: "- No recent interaction events were retained.";
const dimensions = context.interactionMemory.interactionDimensions.length
? context.interactionMemory.interactionDimensions
.map(
(dimension) =>
`- ${dimension.label}: ${dimension.score}/3. ${dimension.summary}`,
)
.join("\n")
: "- No strong interaction dimensions were inferred yet.";
return [
`- Summary: ${context.interactionMemory.summary}`,
`- Disposition: ${context.interactionMemory.disposition}`,
`- Affinity score: ${context.interactionMemory.affinityScore}`,
`- Psychological profile: ${context.interactionMemory.psychologicalProfile}`,
`- Inferred traits: ${context.interactionMemory.inferredTraits.length > 0 ? context.interactionMemory.inferredTraits.join(", ") : "(none yet)"}`,
"- Interaction dimensions:",
dimensions,
`- Response guidance: ${context.interactionMemory.responseGuidance}`,
`- Direct remembered interactions: ${context.interactionMemory.directInteractionCount}`,
`- Ambient remembered mentions: ${context.interactionMemory.ambientMentionCount}`,
`- Relationship summary: ${context.interactionMemory.summary}`,
`- Current stance: ${context.interactionMemory.disposition}; affinity=${context.interactionMemory.affinityScore}`,
`- Private response guidance (do not reveal): ${context.interactionMemory.responseGuidance}`,
"- Specific remembered incidents:",
recentEvents,
].join("\n");

View file

@ -22,3 +22,4 @@
{"ts":"2026-05-02T18:28:04Z","type":"implementation","status":"ok","note":"Pinned the owner Codex lane to a configurable CODEX_MODEL defaulting to gpt-5.4 because the local CLI still faceplants on gpt-5.5, and relaxed source-grounding from a lexical hard gate into an advisory hint so the model can decide when repo/lore tool use is actually needed."}
{"ts":"2026-05-02T18:41:02Z","type":"bugfix","status":"ok","note":"Direct local_llm replies could emit raw JSON/tool payload fragments as final Discord answers; added structured-output rewrite guard and audit output previews for direct provider responses."}
{"ts":"2026-05-02T18:58:38Z","type":"bugfix","status":"ok","note":"Local LLM could choose search_sources for real-life incident questions and get Ghostlight narrative hits; prompt/tool guidance now biases prior server drama and personal-history questions toward search_history before repo/lore search."}
{"ts":"2026-05-02T19:11:44Z","type":"bugfix","status":"ok","note":"Public local replies were too free to surface internal user-analysis language and too vulnerable to prompt-echo history results; interaction memory scaffolding is now slimmer/private and local history tool results filter echoed current asks before the model sees them."}