src / preprocessor.ts

/**
 * @file preprocessor.ts
 * Prompt preprocessor — silently injects relevant memories into the
 * system context before each user message reaches the model.
 *
 * This is what makes the memory system "feel" persistent:
 * the user never has to say "recall" — the model just knows.
 *
 * Flow:
 *   1. User types a message
 *   2. Preprocessor fires, extracts key terms from the message
 *   3. Retrieves top-N memories by composite score
 *   4. Formats them and prepends to the system prompt
 *   5. Model sees the memories as part of its context
 */

import { configSchematics } from "./config";
import { getSharedInstances } from "./toolsProvider";
import { extractFacts } from "./processing/ai";
import { MAX_INJECTED_CONTEXT_CHARS, MEMORY_SEPARATOR } from "./constants";
import type { PluginController } from "./pluginTypes";
import type { ScoredMemory, MemoryRecord } from "./types";

function readConfig(ctl: PluginController) {
  const c = ctl.getPluginConfig(configSchematics);
  return {
    autoInject: c.get("autoInjectMemories") === "on",
    contextCount: c.get("contextMemoryCount") || 5,
    decayDays: c.get("decayHalfLifeDays") || 30,
    storagePath: c.get("memoryStoragePath") || "",
    enableAI: c.get("enableAIExtraction") === "on",
    activeProject: c.get("activeProject") || "",
  };
}

/** Format memories into a clean context block for injection. */
function formatMemories(memories: Array<ScoredMemory | MemoryRecord>): string {
  if (memories.length === 0) return "";

  const lines: string[] = [];
  let totalChars = 0;

  for (const mem of memories) {
    const scopeTag =
      mem.scope !== "global"
        ? ` [${mem.scope}${mem.project ? `:${mem.project}` : ""}]`
        : "";
    const line = `${mem.content} [${mem.category}${mem.tags.length > 0 ? `, tags: ${mem.tags.join(", ")}` : ""}${scopeTag}]`;
    if (totalChars + line.length > MAX_INJECTED_CONTEXT_CHARS) break;
    lines.push(line);
    totalChars += line.length;
  }

  if (lines.length === 0) return "";

  return [
    `[Persistent Memory — ${lines.length} relevant memories]`,
    `You have the following knowledge about this user from previous conversations:`,
    MEMORY_SEPARATOR + lines.join(MEMORY_SEPARATOR),
    ``,
    `Use this knowledge naturally. Do not mention the memory system unless asked.`,
  ].join("\n");
}

export async function promptPreprocessor(
  ctl: PluginController,
  userMessage: string,
): Promise<string> {
  const cfg = readConfig(ctl);

  if (!cfg.autoInject) return userMessage;

  if (userMessage.length < 10) return userMessage;

  try {
    const { engine, db } = await getSharedInstances(cfg.storagePath);

    const result = engine.retrieve(
      userMessage,
      cfg.contextCount,
      cfg.decayDays,
      false,
    );

    let allMemories: Array<ScoredMemory | MemoryRecord> = [...result.memories];
    if (cfg.activeProject) {
      const projectMems = db.getByProject(cfg.activeProject, cfg.contextCount);
      const existingIds = new Set(result.memories.map((m) => m.id));
      for (const pm of projectMems) {
        if (!existingIds.has(pm.id)) allMemories.push(pm);
      }
      allMemories = allMemories.slice(0, cfg.contextCount);
    }

    if (cfg.enableAI && userMessage.length >= 30) {
      const existingSummary = result.memories.map((m) => m.content).join("; ");
      extractFacts(userMessage, existingSummary)
        .then((facts) => {
          for (const fact of facts) {
            try {
              const id = db.store(
                fact.content,
                fact.category,
                fact.tags,
                fact.confidence,
                "ai-extracted",
              );
              engine.indexMemory(id, fact.content, fact.tags, fact.category);
            } catch {
            }
          }
        })
        .catch(() => {
        });
    }

    if (allMemories.length === 0) return userMessage;

    const context = formatMemories(allMemories);
    if (!context) return userMessage;

    return `${context}\n\n---\n\n${userMessage}`;
  } catch {
    return userMessage;
  }
}