Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions apps/construct/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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`;
Expand Down
6 changes: 3 additions & 3 deletions apps/construct/src/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion apps/construct/src/tools/core/memory-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
3 changes: 3 additions & 0 deletions packages/cairn/src/similarity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Loading