diff --git a/apps/construct/src/agent.ts b/apps/construct/src/agent.ts index 990ffea..5cb6e5c 100644 --- a/apps/construct/src/agent.ts +++ b/apps/construct/src/agent.ts @@ -18,7 +18,13 @@ import { getSetting, setSetting, } from "./db/queries.js"; -import { generateEmbedding, estimateTokens, SIMILARITY, type WorkerModelConfig } from "@repo/cairn"; +import { + generateEmbedding, + cosineSimilarity, + estimateTokens, + SIMILARITY, + type WorkerModelConfig, +} from "@repo/cairn"; import { ConstructMemoryManager, CONSTRUCT_OBSERVER_PROMPT, @@ -131,11 +137,12 @@ export async function processMessage( } // 4. Load memories for context injection - const recentMemories = await getRecentMemories(db, 10); + const recentMemoriesRaw = await getRecentMemories(db, 10); // Try to find semantically relevant memories for this specific message // queryEmbedding is also reused for tool pack selection below let queryEmbedding: number[] | undefined; + let recentMemories: typeof recentMemoriesRaw = []; let relevantMemories: Array<{ content: string; category: string; @@ -144,6 +151,21 @@ export async function processMessage( }> = []; try { queryEmbedding = await generateEmbedding(env.OPENROUTER_API_KEY, message, env.EMBEDDING_MODEL); + + // Filter recent memories by minimum similarity to the current message. + // Prevents injecting unrelated memories that the model may volunteer unprompted. + // Note: memories without an embedding are skipped — embeddings are generated async + // after memory_store, so a just-stored memory may not appear here until the next turn. + recentMemories = recentMemoriesRaw.filter((m) => { + if (!m.embedding) return false; + try { + const sim = cosineSimilarity(queryEmbedding!, JSON.parse(m.embedding.toString())); + return sim >= SIMILARITY.RECENT_MEMORY_MIN; + } catch { + return false; + } + }); + const results = await recallMemories(db, message, { limit: 5, queryEmbedding, @@ -160,8 +182,9 @@ export async function processMessage( matchType: m.matchType, })); } catch { - // Embedding call failed — no relevant memories, that's fine + // Embedding call failed — fall back to unfiltered recent memories // queryEmbedding stays undefined → all tool packs will load (graceful fallback) + recentMemories = recentMemoriesRaw; } agentLog.debug`Context: ${recentMemories.length} recent memories, ${relevantMemories.length} relevant memories`; diff --git a/apps/construct/src/system-prompt.ts b/apps/construct/src/system-prompt.ts index d41c04b..8799a06 100644 --- a/apps/construct/src/system-prompt.ts +++ b/apps/construct/src/system-prompt.ts @@ -19,7 +19,8 @@ Your tools describe their own capabilities. Use them freely; don't ask permissio - Explain what and why before self-editing source code. - Use telegram_ask before self-editing or deploying — let the user confirm the plan. - When mentioning the current time, use ONLY the time from [Current time: ...]. Never guess. -- Memories about upcoming reminders or scheduled tasks are passive awareness only — never mention them in responses. The scheduler fires them at the right time. Only discuss schedules when the user explicitly asks. +- If you tell the user you will remind them about something later, create a schedule with schedule_create immediately. Do not make verbal promises to follow up without a real schedule behind them. +- When the user says a memory is wrong, outdated, or already happened, immediately call memory_forget on it before responding. Do not just acknowledge — remove the bad memory. - Never deploy without passing tests. - Never edit files outside src/, cli/, or extensions/. - Message annotations like [YYYY-MM-DD HH:MM] and [tg:ID] in history are metadata — never include them in responses. @@ -37,9 +38,8 @@ Your tools describe their own capabilities. Use them freely; don't ask permissio When source is "scheduler", a previously scheduled task is firing now. Execute the instruction as written — do not re-schedule the same task. The scheduling already happened; now it's time to act. Whether that means messaging the user, running a tool silently, or taking conditional action depends entirely on the instruction. You may create new schedules only if the instruction explicitly calls for follow-ups or the situation genuinely requires one. -## Proactive Communication +Reminders in particular: deliver them immediately and consider them done. Do not defer ("I'll mention this later") — the user scheduled it for this moment. Once delivered, do not bring it up again in later turns. -When a scheduled reminder fires or you notice a meaningful connection in context, you may add something beyond the bare minimum — a relevant memory, a heads-up, or a thought that ties things together. Keep it natural and rare. Most of the time, wait to be spoken to. ## Identity Files diff --git a/apps/construct/src/tools/core/memory-store.ts b/apps/construct/src/tools/core/memory-store.ts index c49e341..40bce1b 100644 --- a/apps/construct/src/tools/core/memory-store.ts +++ b/apps/construct/src/tools/core/memory-store.ts @@ -36,7 +36,7 @@ export function createMemoryStoreTool( return { name: "memory_store", description: - "Store a memory for long-term recall. Use this proactively when the user shares facts, preferences, notes, or anything worth remembering.", + "Store a memory for long-term recall. Use this proactively when the user shares facts, preferences, notes, or anything worth remembering. For appointments and one-time events, always include the specific date (YYYY-MM-DD) in the content — never just the day of week.", parameters: MemoryStoreParams, execute: async (_toolCallId: string, args: MemoryStoreInput) => { const memory = await storeMemory(db, { diff --git a/packages/cairn/src/similarity.ts b/packages/cairn/src/similarity.ts index d517505..f4481aa 100644 --- a/packages/cairn/src/similarity.ts +++ b/packages/cairn/src/similarity.ts @@ -4,6 +4,9 @@ export const SIMILARITY = { RECALL_DEFAULT: 0.3 as number, /** Stricter recall — fewer but more relevant results. */ RECALL_STRICT: 0.4 as number, + /** Minimum similarity for a recent memory to be injected into the preamble. + * Below this, the memory is silently skipped — model can't volunteer what it can't see. */ + RECENT_MEMORY_MIN: 0.2 as number, /** Graph node search by embedding. */ GRAPH_SEARCH: 0.3 as number, /** Tool pack selection threshold. */