fix the memory adding
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-06-26 16:32:34 +01:00
parent 1e0a4c97b3
commit acea7dcf12
8 changed files with 653 additions and 15 deletions

View File

@@ -21,6 +21,12 @@ type ChatSessionData = {
type SendChatResponse = {
memorySuggestions?: unknown[];
memoryAction?: {
type: string;
intent?: string;
status?: string;
error?: string;
};
};
export function useMentorChat(sessionId: string | null) {
@@ -64,12 +70,18 @@ export function useMentorChat(sessionId: string | null) {
setInput("");
setPendingState(createPendingStateOnSend(content));
},
onSuccess: () => {
onSuccess: (data) => {
setPendingState(null);
queryClient.invalidateQueries({ queryKey: ["chat-session", sessionId] });
queryClient.invalidateQueries({ queryKey: ["chat-sessions"] });
queryClient.invalidateQueries({ queryKey: ["ai-memory-suggestions"] });
queryClient.invalidateQueries({ queryKey: ["ai-knows"] });
if (
data.memoryAction?.type === "memory_created" ||
data.memoryAction?.type === "memory_suggestion_accepted"
) {
queryClient.invalidateQueries({ queryKey: ["ai-memory"] });
}
},
onError: (_err, content) => {
setPendingState(createPendingStateOnError(content));

View File

@@ -10,6 +10,7 @@ import {
import { listMemories, getProfileSummary } from "./ai-memory";
import { MEMORY_CATEGORIES, type MemoryCategory } from "@adventureos/shared";
import { extractMemoryCandidates } from "./memory-extraction";
import { buildMemoryActionReply, performChatMemoryAction } from "./chat-memory-actions";
export async function listChatSessions(userId: string) {
return db
@@ -67,6 +68,9 @@ export async function sendChatMessage(userId: string, sessionId: string, content
})
.returning();
const memoryAction = await performChatMemoryAction(userId, sessionId, userMsg, messages);
const authoritativeMemoryReply = buildMemoryActionReply(memoryAction);
const availability = await getAiAvailability(userId);
const ctx = await buildMentorContext(userId, {
userMessage: content,
@@ -89,7 +93,9 @@ export async function sendChatMessage(userId: string, sessionId: string, content
let offline = false;
const memoryIdsUsed = ctx.memoryIds;
if (!availability.canUse || !availability.online) {
if (authoritativeMemoryReply) {
reply = authoritativeMemoryReply;
} else if (!availability.canUse || !availability.online) {
offline = true;
reply = buildOfflineReply(content, ctx);
} else {
@@ -102,6 +108,7 @@ export async function sendChatMessage(userId: string, sessionId: string, content
userMessage: content,
contextBlock,
history,
memoryAction: JSON.stringify(memoryAction),
});
reply =
@@ -116,21 +123,33 @@ export async function sendChatMessage(userId: string, sessionId: string, content
sessionId,
role: "assistant",
content: reply,
metadata: { memoryIdsUsed, offline },
metadata: { memoryIdsUsed, offline, memoryAction },
})
.returning();
const memorySuggestions = await extractMemoryCandidates(userId, "chat", content, {
const memorySuggestions =
memoryAction.intent === "ordinary_message"
? await extractMemoryCandidates(userId, "chat", content, {
type: "ai_chat",
id: userMsg.id,
});
})
: memoryAction.type === "memory_suggestion_created"
? [
{
id: memoryAction.id,
title: memoryAction.title,
category: memoryAction.category,
status: memoryAction.status,
},
]
: [];
await db
.update(aiChatSessions)
.set({ updatedAt: new Date(), title: session.title === "New conversation" ? truncateTitle(content) : session.title })
.where(eq(aiChatSessions.id, sessionId));
return { message: assistantMsg, reply, memoryIdsUsed, offline, memorySuggestions };
return { message: assistantMsg, reply, memoryIdsUsed, offline, memorySuggestions, memoryAction };
}
function truncateTitle(text: string): string {

View File

@@ -241,17 +241,18 @@ export async function createSuggestion(
sourceType: MemorySourceType;
sourceRef?: { type: string; id?: string; date?: string };
confidence?: number;
}
},
opts: { bypassLearningSettings?: boolean } = {}
) {
if (!(input.category in MEMORY_CATEGORIES)) {
throw new Error("Invalid memory category");
}
const settings = await getMemoryLearningSettings(userId);
if (!settings.learningEnabled) return null;
if (!opts.bypassLearningSettings && !settings.learningEnabled) return null;
const pending = await listSuggestions(userId, "pending");
if (pending.length >= settings.maxPendingSuggestions) return null;
if (!opts.bypassLearningSettings && pending.length >= settings.maxPendingSuggestions) return null;
if (pending.some((s) => isSimilarMemoryText(input, s))) return null;

View File

@@ -503,7 +503,7 @@ export async function isAiAvailable(): Promise<boolean> {
export async function generateChatReply(
userId: string,
input: { userMessage: string; contextBlock: string; history?: string }
input: { userMessage: string; contextBlock: string; history?: string; memoryAction?: string }
): Promise<string | null> {
const behavior = await getAiBehaviorConfig(userId);
if (!behavior.enabled) return null;
@@ -524,6 +524,8 @@ export async function generateChatReply(
const prompt = [
input.history ? `Recent conversation:\n${input.history}\n` : "",
`User: ${input.userMessage}`,
input.memoryAction ? `Application memoryAction result: ${input.memoryAction}` : "",
"Memory guardrail: You must not claim that a memory has been saved, added, approved, rejected, deleted, or changed unless the application memoryAction result confirms it. If no memoryAction result confirms persistence, do not imply that memory persistence happened.",
"If the user asks whether you can learn about them, explain that AdventureOS can suggest memories from goals, preferences, routines, worries, and direct remember requests; the user can review, edit, accept, reject, or ignore suggestions before they become saved memory.",
"Respond as the mentor in plain text (no JSON). Keep it concise for a local model.",
]

View File

@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { buildMemoryActionReply, type ChatMemoryActionResult } from "./chat-memory-actions";
describe("buildMemoryActionReply", () => {
it("acknowledges pending suggestions without claiming confirmed save", () => {
const reply = buildMemoryActionReply({
type: "memory_suggestion_created",
intent: "explicit_save_memory",
title: "Wake Up at 7am Every Morning",
category: "current_goals",
status: "pending",
});
expect(reply).toBe('I\'ve created a memory suggestion for review: "Wake Up at 7am Every Morning".');
});
it("acknowledges accepted suggestions as confirmed memory", () => {
const reply = buildMemoryActionReply({
type: "memory_suggestion_accepted",
intent: "approve_pending_memory",
title: "Wake Up at 7am Every Morning",
category: "current_goals",
status: "accepted",
});
expect(reply).toBe("Saved - that goal memory is now part of your confirmed memory.");
});
it("does not claim success on memory failures", () => {
const action: ChatMemoryActionResult = {
type: "memory_action_failed",
intent: "explicit_save_memory",
error: "database unavailable",
};
expect(buildMemoryActionReply(action)).toBe(
"I understood that you wanted to save this as a memory, but I could not save it. Please try again."
);
});
});

View File

@@ -0,0 +1,388 @@
import { and, eq } from "drizzle-orm";
import type { MemoryCategory, MemorySourceType } from "@adventureos/shared";
import { SENSITIVE_MEMORY_CATEGORIES } from "@adventureos/shared";
import { db, aiChatMessages, aiMemorySuggestions } from "../db";
import {
acceptSuggestion,
createMemory,
createSuggestion,
getMemoryLearningSettings,
listMemories,
listSuggestions,
rejectSuggestion,
type MemoryInput,
} from "./ai-memory";
import {
detectMemoryIntent,
type MemoryIntent,
type MemorySuggestionCandidate,
} from "./memory-extraction";
type ChatMessage = typeof aiChatMessages.$inferSelect;
type MemoryActionType =
| "memory_created"
| "memory_suggestion_created"
| "memory_suggestion_accepted"
| "memory_suggestion_rejected"
| "no_memory_action_needed"
| "memory_action_failed";
export type ChatMemoryActionResult = {
type: MemoryActionType;
intent: MemoryIntent;
id?: string;
title?: string;
category?: string;
status?: string;
reason?: string;
error?: string;
};
export async function performChatMemoryAction(
userId: string,
sessionId: string,
userMessage: ChatMessage,
previousMessages: ChatMessage[]
): Promise<ChatMemoryActionResult> {
const detected = detectMemoryIntent(userMessage.content);
try {
if (detected.intent === "explicit_save_memory") {
if (!detected.candidate) {
return {
type: "memory_action_failed",
intent: detected.intent,
reason: "no_memory_candidate",
error: "I understood that you wanted to save a memory, but I could not identify the memory text.",
};
}
return saveExplicitMemory(userId, detected.candidate, {
type: "chat",
sourceRef: { type: "ai_chat", id: userMessage.id },
});
}
if (detected.intent === "approve_pending_memory") {
const pending = await findLatestPendingSuggestionForConversation(userId, sessionId);
if (pending) {
const memory = await acceptSuggestion(userId, pending.id, {
category: detected.category,
});
return {
type: "memory_suggestion_accepted",
intent: detected.intent,
id: memory.id,
title: memory.title,
category: memory.category,
status: "accepted",
};
}
const recentCandidate = findLatestExplicitCandidate(previousMessages);
if (recentCandidate) {
return createAndAcceptFallback(userId, recentCandidate, {
type: "chat",
sourceRef: { type: "ai_chat", id: userMessage.id },
category: detected.category,
intent: detected.intent,
});
}
return {
type: "no_memory_action_needed",
intent: detected.intent,
reason: "no_pending_suggestion",
};
}
if (detected.intent === "reject_pending_memory") {
const pending = await findLatestPendingSuggestionForConversation(userId, sessionId);
if (!pending) {
return {
type: "no_memory_action_needed",
intent: detected.intent,
reason: "no_pending_suggestion",
};
}
await rejectSuggestion(userId, pending.id);
return {
type: "memory_suggestion_rejected",
intent: detected.intent,
id: pending.id,
title: pending.title,
category: pending.category,
status: "rejected",
};
}
if (detected.intent === "edit_pending_memory") {
return {
type: "no_memory_action_needed",
intent: detected.intent,
reason: "edit_not_supported_in_chat",
};
}
return { type: "no_memory_action_needed", intent: detected.intent };
} catch (error) {
return {
type: "memory_action_failed",
intent: detected.intent,
error: error instanceof Error ? error.message : "Memory action failed",
};
}
}
export function buildMemoryActionReply(action: ChatMemoryActionResult): string | null {
if (action.type === "memory_suggestion_created") {
return `I've created a memory suggestion for review: "${action.title}".`;
}
if (action.type === "memory_created") {
return `I've saved that as a ${formatCategory(action.category)} memory: "${action.title}".`;
}
if (action.type === "memory_suggestion_accepted") {
return `Saved - that ${formatCategory(action.category)} memory is now part of your confirmed memory.`;
}
if (action.type === "memory_suggestion_rejected") {
return `Rejected - I did not save that memory suggestion.`;
}
if (action.type === "memory_action_failed") {
return action.error?.startsWith("I understood")
? action.error
: "I understood that you wanted to save this as a memory, but I could not save it. Please try again.";
}
if (action.intent === "approve_pending_memory" && action.reason === "no_pending_suggestion") {
return "I do not have a pending memory suggestion to approve in this chat.";
}
if (action.intent === "reject_pending_memory" && action.reason === "no_pending_suggestion") {
return "I do not have a pending memory suggestion to reject in this chat.";
}
if (action.intent === "edit_pending_memory" && action.reason === "edit_not_supported_in_chat") {
return "I can help with that, but I did not change the memory. Please edit the pending suggestion card before approving it.";
}
if (action.reason === "duplicate_memory") {
return `I did not create a duplicate. That memory is already ${action.status === "pending" ? "pending review" : "saved"}.`;
}
return null;
}
async function saveExplicitMemory(
userId: string,
candidate: MemorySuggestionCandidate,
source: { type: MemorySourceType; sourceRef: { type: string; id?: string; date?: string } }
): Promise<ChatMemoryActionResult> {
const duplicate = await findDuplicate(userId, candidate);
if (duplicate) {
return {
type: "no_memory_action_needed",
intent: "explicit_save_memory",
id: duplicate.id,
title: duplicate.title,
category: duplicate.category,
status: duplicate.status,
reason: "duplicate_memory",
};
}
const settings = await getMemoryLearningSettings(userId);
const shouldRequireApproval =
settings.requireApproval || SENSITIVE_MEMORY_CATEGORIES.includes(candidate.category);
if (!shouldRequireApproval) {
const memory = await createMemory(userId, toMemoryInput(candidate, source.type, source.sourceRef));
return {
type: "memory_created",
intent: "explicit_save_memory",
id: memory.id,
title: memory.title,
category: memory.category,
status: "confirmed",
};
}
const suggestion = await createSuggestion(
userId,
{
...candidate,
sourceType: source.type,
sourceRef: source.sourceRef,
},
{ bypassLearningSettings: true }
);
if (!suggestion) {
return {
type: "memory_action_failed",
intent: "explicit_save_memory",
reason: "suggestion_not_created",
error: "I understood that you wanted to save this as a memory, but I could not save it. Please try again.",
};
}
return {
type: "memory_suggestion_created",
intent: "explicit_save_memory",
id: suggestion.id,
title: suggestion.title,
category: suggestion.category,
status: suggestion.status,
};
}
async function createAndAcceptFallback(
userId: string,
candidate: MemorySuggestionCandidate,
source: {
type: MemorySourceType;
sourceRef: { type: string; id?: string; date?: string };
category?: MemoryCategory;
intent: MemoryIntent;
}
): Promise<ChatMemoryActionResult> {
const adjusted = source.category ? { ...candidate, category: source.category } : candidate;
const duplicate = await findDuplicate(userId, adjusted);
if (duplicate?.status === "confirmed") {
return {
type: "no_memory_action_needed",
intent: source.intent,
id: duplicate.id,
title: duplicate.title,
category: duplicate.category,
status: duplicate.status,
reason: "duplicate_memory",
};
}
const suggestion = await createSuggestion(
userId,
{
...adjusted,
sourceType: source.type,
sourceRef: source.sourceRef,
},
{ bypassLearningSettings: true }
);
if (!suggestion) {
return {
type: "memory_action_failed",
intent: source.intent,
reason: "suggestion_not_created",
error: "I understood that you wanted to approve this memory, but I could not save it. Please try again.",
};
}
const memory = await acceptSuggestion(userId, suggestion.id);
return {
type: "memory_suggestion_accepted",
intent: source.intent,
id: memory.id,
title: memory.title,
category: memory.category,
status: "accepted",
};
}
async function findLatestPendingSuggestionForConversation(userId: string, sessionId: string) {
const messages = await db
.select({ id: aiChatMessages.id })
.from(aiChatMessages)
.where(eq(aiChatMessages.sessionId, sessionId));
const messageIds = new Set(messages.map((m) => m.id));
const pending = await listSuggestions(userId, "pending");
return (
pending.find((suggestion) => {
const ref = suggestion.sourceRef;
return ref?.type === "ai_chat" && !!ref.id && messageIds.has(ref.id);
}) ?? null
);
}
function findLatestExplicitCandidate(messages: ChatMessage[]): MemorySuggestionCandidate | null {
for (const message of [...messages].reverse()) {
if (message.role !== "user") continue;
const detected = detectMemoryIntent(message.content);
if (detected.intent === "explicit_save_memory" && detected.candidate) {
return detected.candidate;
}
}
return null;
}
async function findDuplicate(userId: string, candidate: MemorySuggestionCandidate) {
const memories = await listMemories(userId, { includeArchived: false });
const memory = memories.find((m) => isSimilarMemory(candidate, m));
if (memory) {
return {
id: memory.id,
title: memory.title,
category: memory.category,
status: "confirmed",
};
}
const suggestions = await db
.select()
.from(aiMemorySuggestions)
.where(and(eq(aiMemorySuggestions.userId, userId), eq(aiMemorySuggestions.status, "pending")));
const suggestion = suggestions.find((s) => isSimilarMemory(candidate, s));
if (suggestion) {
return {
id: suggestion.id,
title: suggestion.title,
category: suggestion.category,
status: "pending",
};
}
return null;
}
function toMemoryInput(
candidate: MemorySuggestionCandidate,
sourceType: MemorySourceType,
sourceRef: { type: string; id?: string; date?: string }
): MemoryInput {
return {
category: candidate.category,
title: candidate.title,
content: candidate.content,
confidence: candidate.confidence,
sourceType,
sourceRef,
userVerified: true,
};
}
function isSimilarMemory(
input: { category: MemoryCategory; title: string; content: string },
existing: { category: string; title: string; content: string }
) {
if (input.category !== existing.category) return false;
const inputTitle = normalizeMemoryText(input.title);
const existingTitle = normalizeMemoryText(existing.title);
const inputContent = normalizeMemoryText(input.content);
const existingContent = normalizeMemoryText(existing.content);
return (
inputTitle === existingTitle ||
inputContent === existingContent ||
inputContent.includes(existingTitle) ||
existingContent.includes(inputTitle)
);
}
function normalizeMemoryText(value: string) {
return value
.toLowerCase()
.replace(/\b7\s*a\.?\s*m\.?\b/g, "7am")
.replace(/[^a-z0-9]+/g, " ")
.replace(/\b(the|a|an|user|users|the user|to|is|are|be)\b/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function formatCategory(category?: string) {
if (category === "current_goals") return "goal";
return category?.replace(/_/g, " ") ?? "personal";
}

View File

@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
detectMemoryIntent,
extractDeterministicMemoryCandidates,
extractMemoryCandidates,
} from "./memory-extraction";
@@ -92,6 +93,61 @@ describe("extractDeterministicMemoryCandidates", () => {
});
});
describe("detectMemoryIntent", () => {
it("detects explicit add-memory requests", () => {
const result = detectMemoryIntent("Remember that I prefer short explanations");
expect(result).toMatchObject({
intent: "explicit_save_memory",
confidence: "high",
});
expect(result.candidate).toMatchObject({
category: "ai_tone",
title: "Short Explanations",
});
});
it("detects the exact wake-up goal request as a current goal", () => {
const result = detectMemoryIntent(
"I want you to add a memory for me, and the memory will be a goal type: One of my goals for the next coming weeks is to wake up at 7 am every morning"
);
expect(result).toMatchObject({
intent: "explicit_save_memory",
category: "current_goals",
confidence: "high",
});
expect(result.candidate).toMatchObject({
category: "current_goals",
title: "Wake Up at 7am Every Morning",
});
expect(result.candidate?.content).toBe(
"One of the user's goals for the next few weeks is to wake up at 7am every morning."
);
});
it("maps approval phrases to approve_pending_memory", () => {
expect(detectMemoryIntent("Yes, as a goal, approved.")).toMatchObject({
intent: "approve_pending_memory",
category: "current_goals",
confidence: "high",
});
});
it("maps rejection phrases to reject_pending_memory", () => {
expect(detectMemoryIntent("No, don't remember that.")).toMatchObject({
intent: "reject_pending_memory",
confidence: "high",
});
});
it("does not trigger memory actions for ordinary chat", () => {
expect(detectMemoryIntent("What should I focus on today?")).toMatchObject({
intent: "ordinary_message",
});
});
});
describe("extractMemoryCandidates", () => {
beforeEach(() => {
learningEnabled = true;

View File

@@ -26,6 +26,20 @@ export type MemorySuggestionCandidate = {
confidence: number;
};
export type MemoryIntent =
| "explicit_save_memory"
| "approve_pending_memory"
| "reject_pending_memory"
| "edit_pending_memory"
| "ordinary_message";
export type MemoryIntentResult = {
intent: MemoryIntent;
candidate?: MemorySuggestionCandidate;
category?: MemoryCategory;
confidence: "high" | "medium" | "low";
};
type PatternRule = {
category: MemoryCategory;
confidence: number;
@@ -95,6 +109,47 @@ const RULES: PatternRule[] = [
const LEARNING_HINTS = /\b(learn|learning|examples?|explanations?|teach|lesson)\b/i;
const EXERCISE_HINTS = /\b(exercise|work out|workout|run|gym|walk|train)\b/i;
const UNSPECIFIC_MESSAGES = /^(hello|hi|hey|thanks|thank you|what can you do\??|that's interesting|that is interesting)$/i;
const EXPLICIT_SAVE_PHRASES =
/\b(remember that|remember this as|save this as|save this|add this to memory|add this as|add a memory|i want you to add a memory|the memory will be)\b/i;
const APPROVAL_PHRASES =
/\b(approved|yes approved|yes,?\s+as\s+a\s+\w+,?\s+approved|save it|confirm it|that is correct|that's correct|yes that's right|yes that is right|accept it)\b/i;
const REJECTION_PHRASES =
/\b(reject it|don't save that|do not save that|forget that|ignore that suggestion|no,?\s+don'?t remember that|no,?\s+do not remember that)\b/i;
const EDIT_PHRASES = /\b(change it to|edit that|update that suggestion|make it)\b/i;
export function detectMemoryIntent(sourceText: string): MemoryIntentResult {
const normalized = normalizeInput(sourceText);
if (!normalized) return { intent: "ordinary_message", confidence: "low" };
if (REJECTION_PHRASES.test(normalized)) {
return { intent: "reject_pending_memory", confidence: "high" };
}
if (EXPLICIT_SAVE_PHRASES.test(normalized)) {
const candidateText = extractExplicitMemoryText(normalized);
const candidate = buildExplicitCandidate(candidateText, normalized);
return {
intent: "explicit_save_memory",
candidate,
category: candidate?.category ?? categoryFromMemoryCommand(normalized),
confidence: candidate ? "high" : "medium",
};
}
if (APPROVAL_PHRASES.test(normalized)) {
return {
intent: "approve_pending_memory",
category: categoryFromMemoryCommand(normalized),
confidence: "high",
};
}
if (EDIT_PHRASES.test(normalized)) {
return { intent: "edit_pending_memory", confidence: "medium" };
}
return { intent: "ordinary_message", confidence: "low" };
}
export function extractDeterministicMemoryCandidates(sourceText: string): MemorySuggestionCandidate[] {
const normalized = normalizeInput(sourceText);
@@ -249,16 +304,20 @@ function isDurableEnough(value: string): boolean {
function refineCategory(category: MemoryCategory, fullText: string, value: string): MemoryCategory {
const combined = `${fullText} ${value}`;
if (/\b(goal type|as a goal|one of my goals|my goals?|i want to|i'm trying to|i am trying to)\b/i.test(combined)) {
return "current_goals";
}
if (category === "likes" && LEARNING_HINTS.test(combined)) return "learning_interests";
if (category === "current_goals" && EXERCISE_HINTS.test(combined)) return "exercise_preferences";
return category;
}
function refineContent(content: string, fullText: string): string {
const normalizedTime = content.replace(/\b7\s*a\.?\s*m\.?\b/gi, "7am");
if (/one of my goals/i.test(fullText) && /next coming weeks|next few weeks/i.test(fullText)) {
return content.replace("current goals is to", "goals for the next few weeks is to");
return normalizedTime.replace("current goals is to", "goals for the next few weeks is to");
}
return content;
return normalizedTime;
}
function ensureVerbPhrase(value: string): string {
@@ -292,3 +351,64 @@ function buildTitle(value: string, prefix?: string): string {
.join(" ");
return prefix ? `${prefix}: ${title}` : title;
}
function extractExplicitMemoryText(sourceText: string): string {
const afterColon = sourceText.split(":").slice(1).join(":").trim();
if (afterColon) return afterColon;
return sourceText
.replace(/^.*?\b(?:remember that|remember this as|save this as|save this|add this to memory|add this as|add a memory|i want you to add a memory|the memory will be)\b/i, "")
.replace(/^(?:for me|and|the memory will be|a|an|goal type|as a goal|goal)\b[:,\s-]*/i, "")
.trim();
}
function buildExplicitCandidate(candidateText: string, fullText: string): MemorySuggestionCandidate | undefined {
const durableText = stripTrailingPunctuation(candidateText);
const cleaned = cleanValue(candidateText);
if (!isDurableEnough(durableText) && !isDurableEnough(cleaned)) return undefined;
const deterministic = extractDeterministicMemoryCandidates(durableText)[0];
if (deterministic) {
return {
...deterministic,
category: refineCategory(deterministic.category, fullText, cleaned),
content: refineContent(deterministic.content, fullText),
confidence: Math.max(deterministic.confidence, 0.9),
};
}
const category = categoryFromMemoryCommand(fullText);
const value = stripLeadingMemoryType(cleaned);
if (!isDurableEnough(value)) return undefined;
if (category === "current_goals") {
return {
category,
title: buildTitle(value),
content: `One of the user's current goals is to ${normalizeTime(ensureVerbPhrase(value))}.`,
confidence: 0.9,
};
}
return {
category,
title: buildTitle(value),
content: `The user explicitly asked the app to remember that ${normalizeTime(stripTrailingPunctuation(value))}.`,
confidence: 0.9,
};
}
function categoryFromMemoryCommand(value: string): MemoryCategory {
if (/\b(goal type|as a goal|current goal|one of my goals|my goals?|i want to|i'm trying to|i am trying to)\b/i.test(value)) {
return "current_goals";
}
return "personal_context";
}
function stripLeadingMemoryType(value: string): string {
return value.replace(/^(?:goal type|as a goal|goal)\b[:,\s-]*/i, "").trim();
}
function normalizeTime(value: string): string {
return value.replace(/\b7\s*a\.?\s*m\.?\b/gi, "7am");
}