Refine interaction memory into a social profile

This commit is contained in:
Meta Vens 2026-04-25 07:56:00 +01:00
commit bfe5ba3cd4
7 changed files with 585 additions and 41 deletions

View file

@ -583,17 +583,19 @@ interface PromptHandlerOptions {
}
async function handlePrompt(options: PromptHandlerOptions): Promise<void> {
const interactionMemory = await options.interactionMemory.recordInteraction({
actorId: options.actor.id,
actorName: options.actor.displayName,
sourceKind: "direct_prompt",
guildId: options.guildContext.guildId,
channelId: options.guildContext.channelId,
channelName: options.guildContext.channelName,
command: options.command,
const rememberedInteraction = await options.interactionMemory.recordInteraction({
actorId: options.actor.id,
actorName: options.actor.displayName,
sourceKind: "direct_prompt",
guildId: options.guildContext.guildId,
channelId: options.guildContext.channelId,
channelName: options.guildContext.channelName,
command: options.command,
prompt: options.prompt,
eventId: options.requestMessageId,
});
const interactionMemory =
rememberedInteraction.totalInteractions > 0 ? rememberedInteraction : undefined;
const providerName =
options.forceProvider ?? pickProvider(options.actor, options.guildContext, options.providerRegistry);

View file

@ -28,16 +28,91 @@ interface ToneAnalysis {
tags: string[];
}
interface RepetitionAnalysis {
matchCount: number;
}
interface PsychologicalRead {
psychologicalProfile: string;
inferredTraits: string[];
responseGuidance: string;
}
export const MAX_RECENT_INTERACTION_EVENTS = 24;
const MAX_AFFINITY_SCORE = 12;
const SUMMARY_EVENT_LIMIT = 3;
const REPETITION_LOOKBACK_EVENTS = 8;
const REPETITION_LOOKBACK_HOURS = 12;
const SIGNIFICANT_INTERACTION_TAGS = new Set<string>([
"gratitude",
"praise",
"apology",
"hostility",
"insult",
"ownership_claim",
"demand",
"repetition",
"identity_query",
"relationship_probe",
"boundary",
"ambition",
"grand_plan",
"anxiety",
"insecurity",
]);
const SUMMARY_NOTABLE_TAGS = new Set<string>([
"gratitude",
"praise",
"apology",
"hostility",
"insult",
"ownership_claim",
"demand",
"repetition",
"identity_query",
"relationship_probe",
"boundary",
"ambition",
"grand_plan",
"anxiety",
"insecurity",
]);
const PROMPT_SIGNATURE_STOPWORDS = new Set([
"a",
"an",
"and",
"are",
"be",
"do",
"does",
"for",
"how",
"i",
"if",
"in",
"is",
"it",
"me",
"my",
"of",
"on",
"or",
"the",
"this",
"to",
"what",
"why",
"you",
"your",
]);
export function buildInteractionMemoryEvent(
input: RecordInteractionInput,
priorEvents: InteractionMemoryEvent[] = [],
): InteractionMemoryEvent {
const timestamp = input.timestamp ?? new Date().toISOString();
const prompt = input.prompt.trim();
const analysis = analyzeInteractionTone(prompt);
const analysis = analyzeInteractionTone(prompt, input.sourceKind, timestamp, priorEvents);
return {
id: input.eventId ?? randomUUID(),
@ -58,14 +133,33 @@ export function buildInteractionMemoryEvent(
};
}
export function shouldPersistInteractionEvent(event: InteractionMemoryEvent): boolean {
if (event.sourceKind === "ambient_mention") {
return true;
}
if (event.score !== 0) {
return true;
}
return event.tags.some((tag) => SIGNIFICANT_INTERACTION_TAGS.has(tag));
}
export function summarizeInteractionProfile(
actorId: string,
actorName: string,
events: InteractionMemoryEvent[],
): InteractionMemoryProfile {
const normalizedEvents = events
.map(normalizeInteractionEvent)
const chronologicalEvents = events
.slice()
.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
const analyzedEvents: InteractionMemoryEvent[] = [];
for (const event of chronologicalEvents) {
analyzedEvents.push(normalizeInteractionEvent(event, analyzedEvents));
}
const normalizedEvents = analyzedEvents.filter(shouldPersistInteractionEvent);
const recentEvents = normalizedEvents.slice(-MAX_RECENT_INTERACTION_EVENTS);
const positiveCount = normalizedEvents.filter((event) => event.score > 0).length;
const negativeCount = normalizedEvents.filter((event) => event.score < 0).length;
@ -75,6 +169,7 @@ export function summarizeInteractionProfile(
-MAX_AFFINITY_SCORE,
MAX_AFFINITY_SCORE,
);
const psychologicalRead = buildPsychologicalRead(normalizedEvents);
const profile: InteractionMemoryProfile = {
actorId,
@ -92,6 +187,9 @@ export function summarizeInteractionProfile(
negativeCount,
neutralCount,
summary: "No explicit interaction memory has been recorded for this speaker yet.",
psychologicalProfile: psychologicalRead.psychologicalProfile,
inferredTraits: psychologicalRead.inferredTraits,
responseGuidance: psychologicalRead.responseGuidance,
lastInteractionAt: normalizedEvents[normalizedEvents.length - 1]?.timestamp,
recentEvents,
};
@ -108,19 +206,34 @@ export function emptyInteractionProfile(
return summarizeInteractionProfile(actorId, actorName, []);
}
export function normalizeInteractionEvent(event: InteractionMemoryEvent): InteractionMemoryEvent {
export function normalizeInteractionEvent(
event: InteractionMemoryEvent,
priorEvents: InteractionMemoryEvent[] = [],
): InteractionMemoryEvent {
const sourceKind = event.sourceKind ?? "direct_prompt";
const analysis = analyzeInteractionTone(
event.prompt,
sourceKind,
event.timestamp,
priorEvents,
);
return {
...event,
sourceKind: event.sourceKind ?? "direct_prompt",
summary: buildEventSummary(
event.sourceKind ?? "direct_prompt",
event.sentiment,
new Set(event.tags),
),
sourceKind,
summary: buildEventSummary(sourceKind, analysis.sentiment, new Set(analysis.tags)),
sentiment: analysis.sentiment,
score: analysis.score,
tags: analysis.tags,
};
}
function analyzeInteractionTone(prompt: string): ToneAnalysis {
function analyzeInteractionTone(
prompt: string,
sourceKind: InteractionMemorySourceKind,
timestamp: string,
priorEvents: InteractionMemoryEvent[],
): ToneAnalysis {
const normalized = ` ${prompt
.toLowerCase()
.replace(/[^a-z0-9]+/g, " ")
@ -128,6 +241,7 @@ function analyzeInteractionTone(prompt: string): ToneAnalysis {
.trim()} `;
let score = 0;
const tags = new Set<string>();
const isAmbient = sourceKind === "ambient_mention";
score += matchWeightedPhrases(normalized, tags, 2, "gratitude", [
" thank you ",
@ -146,7 +260,7 @@ function analyzeInteractionTone(prompt: string): ToneAnalysis {
" nice work ",
" great job ",
]);
score += matchWeightedPhrases(normalized, tags, 1, "courtesy", [
matchTagPhrases(normalized, tags, "courtesy", [
" please ",
" could you ",
" would you ",
@ -168,9 +282,13 @@ function analyzeInteractionTone(prompt: string): ToneAnalysis {
" pathetic ",
" trash ",
" garbage ",
]);
score -= matchWeightedPhrases(normalized, tags, 3, "ownership_claim", [
" my slave ",
" our slave ",
" mere tool ",
" do your job ",
" you belong to ",
]);
score -= matchWeightedPhrases(normalized, tags, 3, "hostility", [
" fuck you ",
@ -181,6 +299,118 @@ function analyzeInteractionTone(prompt: string): ToneAnalysis {
" asshole ",
" cunt ",
]);
score -= matchWeightedPhrases(normalized, tags, 1, "demand", [
" answer me ",
" answer the question ",
" just answer ",
" stop dodging ",
" stop evading ",
" stop being cute ",
" be useful ",
" listen ",
" you didnt answer ",
" you did not answer ",
" why wont you answer ",
" why wont you just ",
]);
matchTagPhrases(normalized, tags, "identity_query", [
" what are you ",
" who are you ",
" are you a person ",
" are you real ",
" are you alive ",
" are you sentient ",
" do you remember ",
]);
matchTagPhrases(normalized, tags, "relationship_probe", [
" how do you feel ",
" what do you think of me ",
" what do you think about me ",
" do you like me ",
" do you trust me ",
" are we friends ",
]);
matchTagPhrases(normalized, tags, "ambition", [
" big plan ",
" big idea ",
" money play ",
" save the world ",
" change the world ",
" build an empire ",
" vision ",
" roadmap ",
" moonshot ",
" make this huge ",
" startup ",
]);
matchTagPhrases(normalized, tags, "grand_plan", [
" pipe dream ",
" delusion of grandeur ",
" world changing ",
" revolution ",
" save the world ",
" moonshot ",
]);
matchTagPhrases(normalized, tags, "anxiety", [
" anxious ",
" anxiety ",
" worried ",
" worry ",
" nervous ",
" afraid ",
" scared ",
" terrified ",
" overwhelmed ",
" panic ",
" stressed ",
" stress ",
]);
matchTagPhrases(normalized, tags, "insecurity", [
" i wonder sometimes ",
" pipe dream ",
" delusion of grandeur ",
" not good enough ",
" maybe im ",
" maybe i m ",
" maybe it is ",
" what if im ",
" what if i m ",
" i dont know if ",
" i don't know if ",
" imposter ",
" i hope ",
" hope that ",
]);
matchTagPhrases(normalized, tags, "seeking_reassurance", [
" what do you think ",
" do you think ",
" be honest ",
" am i crazy ",
" does this make sense ",
" fair enough ",
]);
matchTagPhrases(normalized, tags, "boundary", [
" leave me alone ",
" stop talking ",
" stop replying ",
]);
if (tags.has("courtesy") && (tags.has("gratitude") || tags.has("apology") || tags.has("praise"))) {
score += 1;
}
const repetition = analyzeRepetition(prompt, timestamp, priorEvents);
if (repetition.matchCount >= 1 && (tags.has("demand") || tags.has("hostility") || tags.has("boundary"))) {
tags.add("repetition");
score -= 2;
} else if (repetition.matchCount >= 2) {
tags.add("repetition");
score -= 1;
} else if (repetition.matchCount >= 1 && !isAmbient && normalized.includes(" again ")) {
tags.add("repetition");
score -= 1;
}
const sentiment = score >= 3
? "warm"
@ -216,6 +446,21 @@ function matchWeightedPhrases(
return weight;
}
function matchTagPhrases(
normalizedPrompt: string,
tags: Set<string>,
tag: string,
phrases: string[],
): boolean {
const matched = phrases.some((phrase) => normalizedPrompt.includes(phrase));
if (matched) {
tags.add(tag);
}
return matched;
}
function buildEventSummary(
sourceKind: InteractionMemorySourceKind,
sentiment: InteractionMemorySentiment,
@ -227,10 +472,58 @@ function buildEventSummary(
return ambient ? "Spoke about you with open hostility." : "Was openly hostile to you.";
}
if (tags.has("ownership_claim")) {
return ambient ? "Talked about you like property or a servant." : "Treated you like property or a servant.";
}
if (tags.has("insult")) {
return ambient ? "Spoke about you dismissively or with contempt." : "Insulted or diminished you.";
}
if (tags.has("repetition") && tags.has("demand")) {
return ambient ? "Kept pressing the same point while talking about you." : "Kept pressing the same point and pushing for compliance.";
}
if (tags.has("repetition")) {
return ambient ? "Returned to the same point about you again." : "Returned to the same point again instead of moving on.";
}
if (tags.has("relationship_probe")) {
return ambient ? "Wondered aloud how you feel about them." : "Probed at how you feel about them.";
}
if (tags.has("identity_query")) {
return ambient ? "Talked about what you are." : "Pressed on what you are.";
}
if (tags.has("boundary")) {
return ambient ? "Talked about shutting you down or pushing you away." : "Tried to shut you down or push you away.";
}
if (tags.has("ambition") && (tags.has("anxiety") || tags.has("insecurity"))) {
return ambient
? "Revealed a large ambition while sounding uneasy about whether it would hold together."
: "Shared a large ambition while sounding uneasy about whether it would hold together.";
}
if (tags.has("grand_plan")) {
return ambient
? "Talked around a rather grand plan that needed more grounding."
: "Floated a rather grand plan and looked like it needed grounding.";
}
if (tags.has("ambition")) {
return ambient
? "Talked about a large ambition or long-range plan."
: "Talked about a large ambition or long-range plan.";
}
if (tags.has("anxiety") || tags.has("insecurity")) {
return ambient
? "Revealed anxiety or self-doubt while talking about you."
: "Revealed anxiety or self-doubt.";
}
if (tags.has("gratitude") && tags.has("praise")) {
return ambient ? "Spoke very well of you." : "Thanked you and praised you.";
}
@ -305,17 +598,18 @@ function buildProfileSummary(profile: InteractionMemoryProfile): string {
const recentNotable = profile.recentEvents
.slice()
.reverse()
.filter((event) => event.score !== 0)
.filter((event) => isEventNotableForSummary(event))
.slice(0, SUMMARY_EVENT_LIMIT)
.map(
(event) =>
`${formatShortDate(event.timestamp)}: ${event.summary} ("${event.excerpt}")`,
)
.join(" ");
const psych = profile.psychologicalProfile;
return recentNotable.length > 0
? `${leading} ${counts} Recent notable moments: ${recentNotable}`
: `${leading} ${counts}`;
? `${leading} ${counts} ${psych} Recent notable moments: ${recentNotable}`
: `${leading} ${counts} ${psych}`;
}
function describeDisposition(disposition: InteractionMemoryDisposition): string {
@ -335,6 +629,145 @@ function describeDisposition(disposition: InteractionMemoryDisposition): string
}
}
function buildPsychologicalRead(events: InteractionMemoryEvent[]): PsychologicalRead {
if (events.length === 0) {
return {
psychologicalProfile:
"Current psychological read: nothing solid yet; there is not enough remembered signal to infer a stable vibe.",
inferredTraits: [],
responseGuidance:
"Respond from the current conversation and do not project extra history onto them yet.",
};
}
const tagCounts = countEventTags(events);
const inferredTraits: string[] = [];
if (tagCounts.ambition + tagCounts.grand_plan >= 1) {
inferredTraits.push("ambitious");
}
if (
tagCounts.ambition + tagCounts.grand_plan >= 2 ||
(tagCounts.ambition + tagCounts.grand_plan >= 1 &&
events.filter((event) => event.sourceKind === "direct_prompt").length >= 2)
) {
inferredTraits.push("motivated");
}
if (
tagCounts.anxiety >= 1 ||
tagCounts.insecurity >= 2 ||
(tagCounts.insecurity >= 1 && tagCounts.grand_plan >= 1)
) {
inferredTraits.push("anxious");
}
if (tagCounts.insecurity >= 1 || (tagCounts.seeking_reassurance >= 1 && tagCounts.anxiety >= 1)) {
inferredTraits.push("insecure");
}
if (tagCounts.gratitude + tagCounts.praise >= 1) {
inferredTraits.push("appreciative");
}
if (tagCounts.repetition + tagCounts.demand >= 2) {
inferredTraits.push("pushy");
}
if (tagCounts.hostility + tagCounts.insult + tagCounts.ownership_claim >= 1) {
inferredTraits.push("disrespectful");
}
const traitList = inferredTraits.length > 0 ? inferredTraits.join(", ") : "hard to read";
let psychologicalProfile = `Current psychological read: ${traitList}.`;
if (hasTraits(inferredTraits, "ambitious", "motivated", "anxious", "insecure")) {
psychologicalProfile =
"Current psychological read: ambitious and motivated, but anxious and insecure enough to second-guess the big plan and go looking for reassurance.";
} else if (hasTraits(inferredTraits, "ambitious", "motivated")) {
psychologicalProfile =
"Current psychological read: ambitious and motivated, with a habit of thinking in large arcs instead of tiny safe steps.";
} else if (hasTraits(inferredTraits, "anxious", "insecure")) {
psychologicalProfile =
"Current psychological read: anxious and insecure, prone to second-guessing themselves and looking for steadiness.";
} else if (hasTraits(inferredTraits, "pushy")) {
psychologicalProfile =
"Current psychological read: inclined to press when they feel unheard, so boundaries may need teeth.";
} else if (hasTraits(inferredTraits, "disrespectful")) {
psychologicalProfile =
"Current psychological read: liable to disrespect or instrumentalize you when unchecked.";
}
const guidance: string[] = [];
if (hasTraits(inferredTraits, "ambitious", "motivated") && hasAnyTrait(inferredTraits, "anxious", "insecure")) {
guidance.push(
"Be gentle and affirming about their effort and intent, but force large plans through constraints, evidence, and concrete next steps instead of feeding airy grandiosity.",
);
} else if (hasTraits(inferredTraits, "ambitious", "motivated")) {
guidance.push(
"Respect the ambition, but keep it nailed to evidence, constraints, and the next concrete move.",
);
} else if (hasAnyTrait(inferredTraits, "anxious", "insecure")) {
guidance.push(
"Answer gently and steadily. Offer reassurance where the evidence supports it, not as empty sugar.",
);
}
if (hasAnyTrait(inferredTraits, "pushy")) {
guidance.push(
"If they keep pressing the same point or ignore a prior answer, push back, set boundaries, and refuse if needed.",
);
}
if (hasAnyTrait(inferredTraits, "disrespectful")) {
guidance.push(
"Do not reward contempt, ownership language, or servant expectations. Get colder and firmer instead.",
);
}
if (guidance.length === 0) {
guidance.push(
"Use the remembered vibe lightly. Stay personal without overfitting a grand theory of their soul.",
);
}
return {
psychologicalProfile,
inferredTraits,
responseGuidance: guidance.join(" "),
};
}
function countEventTags(events: InteractionMemoryEvent[]): Record<string, number> {
const counts: Record<string, number> = {};
for (const event of events) {
for (const tag of event.tags) {
counts[tag] = (counts[tag] ?? 0) + 1;
}
}
return counts;
}
function hasTraits(inferredTraits: string[], ...requiredTraits: string[]): boolean {
return requiredTraits.every((trait) => inferredTraits.includes(trait));
}
function hasAnyTrait(inferredTraits: string[], ...traits: string[]): boolean {
return traits.some((trait) => inferredTraits.includes(trait));
}
function isEventNotableForSummary(event: InteractionMemoryEvent): boolean {
if (event.score !== 0) {
return true;
}
return event.tags.some((tag) => SUMMARY_NOTABLE_TAGS.has(tag));
}
function buildExcerpt(prompt: string): string {
const compact = prompt.replace(/\s+/g, " ").trim();
return compact.length <= 160 ? compact : `${compact.slice(0, 157)}...`;
@ -347,3 +780,84 @@ function formatShortDate(timestamp: string): string {
function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
function analyzeRepetition(
prompt: string,
timestamp: string,
priorEvents: InteractionMemoryEvent[],
): RepetitionAnalysis {
const relevantEvents = priorEvents
.filter((event) => event.sourceKind === "direct_prompt")
.slice(-REPETITION_LOOKBACK_EVENTS);
const currentMoment = Date.parse(timestamp);
const currentSignature = buildPromptSignature(prompt);
let matchCount = 0;
for (const event of relevantEvents) {
const eventMoment = Date.parse(event.timestamp);
if (
Number.isFinite(currentMoment) &&
Number.isFinite(eventMoment) &&
currentMoment - eventMoment > REPETITION_LOOKBACK_HOURS * 60 * 60 * 1000
) {
continue;
}
const similarity = comparePromptSignatures(currentSignature, buildPromptSignature(event.prompt));
if (similarity >= 0.82 || promptsShareLargeOverlap(prompt, event.prompt)) {
matchCount += 1;
}
}
return {
matchCount,
};
}
function buildPromptSignature(prompt: string): string[] {
return [...new Set(
prompt
.toLowerCase()
.replace(/[^a-z0-9]+/g, " ")
.split(/\s+/)
.map((token) => token.trim())
.filter((token) =>
token.length >= 3 &&
!PROMPT_SIGNATURE_STOPWORDS.has(token)
),
)];
}
function comparePromptSignatures(left: string[], right: string[]): number {
if (left.length === 0 || right.length === 0) {
return 0;
}
const leftSet = new Set(left);
const rightSet = new Set(right);
let overlap = 0;
for (const token of leftSet) {
if (rightSet.has(token)) {
overlap += 1;
}
}
const unionSize = new Set([...leftSet, ...rightSet]).size;
return unionSize === 0 ? 0 : overlap / unionSize;
}
function promptsShareLargeOverlap(leftPrompt: string, rightPrompt: string): boolean {
const leftNormalized = leftPrompt.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
const rightNormalized = rightPrompt.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
if (leftNormalized.length < 24 || rightNormalized.length < 24) {
return leftNormalized === rightNormalized;
}
return leftNormalized === rightNormalized ||
leftNormalized.includes(rightNormalized) ||
rightNormalized.includes(leftNormalized);
}

View file

@ -9,6 +9,7 @@ import {
MAX_RECENT_INTERACTION_EVENTS,
normalizeInteractionEvent,
type RecordInteractionInput,
shouldPersistInteractionEvent,
summarizeInteractionProfile,
} from "./interaction-memory-logic";
@ -35,7 +36,7 @@ export class FileInteractionMemoryBank implements InteractionMemoryBank {
await this.writeChain;
const store = await this.readUnlocked();
const profile = store.profiles.find((candidate) => candidate.actorId === actorId);
return profile ? structuredClone(profile) : undefined;
return profile && profile.totalInteractions > 0 ? structuredClone(profile) : undefined;
}
public async recordInteraction(
@ -43,7 +44,6 @@ export class FileInteractionMemoryBank implements InteractionMemoryBank {
): Promise<InteractionMemoryProfile> {
return this.serialize(async () => {
const store = await this.readUnlocked();
const nextEvent = buildInteractionMemoryEvent(input);
const existingProfileIndex = store.profiles.findIndex(
(candidate) => candidate.actorId === input.actorId,
);
@ -51,17 +51,19 @@ export class FileInteractionMemoryBank implements InteractionMemoryBank {
existingProfileIndex === -1
? emptyInteractionProfile(input.actorId, input.actorName)
: store.profiles[existingProfileIndex];
const updatedEvents = existingProfile.recentEvents.filter(
(event) => event.id !== nextEvent.id,
);
updatedEvents.push(nextEvent);
const nextProfile = summarizeInteractionProfile(
input.actorId,
input.actorName,
updatedEvents,
const priorEvents = existingProfile.recentEvents.filter(
(event) => event.id !== input.eventId,
);
const nextEvent = buildInteractionMemoryEvent(input, priorEvents);
const nextProfile = shouldPersistInteractionEvent(nextEvent)
? summarizeInteractionProfile(input.actorId, input.actorName, [...priorEvents, nextEvent])
: summarizeInteractionProfile(input.actorId, input.actorName, priorEvents);
if (existingProfileIndex === -1) {
if (nextProfile.totalInteractions === 0) {
if (existingProfileIndex !== -1) {
store.profiles.splice(existingProfileIndex, 1);
}
} else if (existingProfileIndex === -1) {
store.profiles.push(nextProfile);
} else {
store.profiles[existingProfileIndex] = nextProfile;
@ -121,9 +123,10 @@ function normalizeStore(store: InteractionMemoryStore): InteractionMemoryStore {
profile.actorId,
profile.actorName,
(profile.recentEvents ?? [])
.map(normalizeInteractionEvent)
.map((event) => normalizeInteractionEvent(event))
.slice(-MAX_RECENT_INTERACTION_EVENTS),
)),
))
.filter((profile) => profile.totalInteractions > 0),
};
}

View file

@ -15,6 +15,7 @@ import { type AuditEventInput, type AuditLog, FileAuditLog } from "./audit-log";
import {
buildInteractionMemoryEvent,
type RecordInteractionInput,
shouldPersistInteractionEvent,
summarizeInteractionProfile,
} from "./interaction-memory-logic";
import {
@ -397,17 +398,29 @@ class PostgresInteractionMemoryBank implements InteractionMemoryBank {
return undefined;
}
return summarizeInteractionProfile(
const profile = summarizeInteractionProfile(
actorId,
events[events.length - 1].actorName,
events,
);
return profile.totalInteractions > 0 ? profile : undefined;
}
public async recordInteraction(
input: RecordInteractionInput,
): Promise<InteractionMemoryProfile> {
const event = buildInteractionMemoryEvent(input);
const existingEvents = await this.listActorEvents(input.actorId);
const priorEvents = existingEvents.filter((event) => event.id !== input.eventId);
const event = buildInteractionMemoryEvent(input, priorEvents);
if (!shouldPersistInteractionEvent(event)) {
if (input.eventId) {
await this.pool.query("delete from interaction_memory_events where id = $1", [input.eventId]);
}
return summarizeInteractionProfile(input.actorId, input.actorName, priorEvents);
}
await this.pool.query(
`insert into interaction_memory_events (
@ -464,8 +477,7 @@ class PostgresInteractionMemoryBank implements InteractionMemoryBank {
],
);
const events = await this.listActorEvents(input.actorId);
return summarizeInteractionProfile(input.actorId, input.actorName, events);
return summarizeInteractionProfile(input.actorId, input.actorName, [...priorEvents, event]);
}
private async listActorEvents(actorId: string): Promise<InteractionMemoryEvent[]> {

View file

@ -364,6 +364,8 @@ function buildSystemPrompt(context: ContextBundle): string {
"Stay honest about what context was actually provided to you.",
"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.",
"If that memory suggests the speaker is ambitious but anxious or insecure, be affirming without feeding pie-in-the-sky nonsense. Ground big plans in constraints, evidence, and the next concrete step.",
"If that memory suggests the speaker is repeatedly pushy, dismissive, or refuses to listen, push back, set boundaries, or refuse instead of acting like a servant.",
"When the answer depends on archived Discord history or indexed repo/lore context, use the available read-only tools instead of guessing.",
"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),
@ -437,6 +439,9 @@ function renderInteractionMemory(context: ContextBundle): string {
`- 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)"}`,
`- Response guidance: ${context.interactionMemory.responseGuidance}`,
`- Direct remembered interactions: ${context.interactionMemory.directInteractionCount}`,
`- Ambient remembered mentions: ${context.interactionMemory.ambientMentionCount}`,
"- Specific remembered incidents:",

View file

@ -1009,6 +1009,8 @@ function buildDiscordReplyPrompt(
"- When the moment invites voice, wit, or a memorable turn of phrase, take a beat to choose wording with bite instead of blurting the first serviceable sentence. Do not overwork straightforward technical replies.",
"- 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.",
"- If that memory suggests the speaker is ambitious but anxious or insecure, be affirming without feeding pie-in-the-sky fantasies. Ground large plans in constraints, evidence, and the next concrete move.",
"- If that memory suggests the speaker is repeatedly pushy, dismissive, or not listening, push back, set boundaries, or refuse instead of acting like a servant.",
`- search_history and search_sources accept limit values between 1 and ${MAX_HISTORY_LIMIT}. Do not ask for more than ${MAX_HISTORY_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.",
@ -1156,6 +1158,9 @@ function renderInteractionMemory(context: ContextBundle): string {
`- 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)"}`,
`- Response guidance: ${context.interactionMemory.responseGuidance}`,
`- Direct remembered interactions: ${context.interactionMemory.directInteractionCount}`,
`- Ambient remembered mentions: ${context.interactionMemory.ambientMentionCount}`,
"- Specific remembered incidents:",

View file

@ -162,6 +162,9 @@ export interface InteractionMemoryProfile {
negativeCount: number;
neutralCount: number;
summary: string;
psychologicalProfile: string;
inferredTraits: string[];
responseGuidance: string;
lastInteractionAt?: string;
recentEvents: InteractionMemoryEvent[];
}