src/store/store.ts

import fs from "node:fs"; import path from "node:path"; import { Database } from "bun:sqlite"; import { clamp, nowIso } from "../utils.ts"; import { minimizeSettingsIntent } from "../settings/settingsIntent.ts"; import { rewriteRuntimeSettingsRow, getSettings, getSettingsRecord, setSettings, patchSettings, patchSettingsWithVersion, replaceSettingsWithVersion, resetSettings } from "./storeSettings.ts"; import { recordMessage, getRecentMessages, getRecentMessagesAcrossGuild, getMessagesInWindow, searchRelevantMessages, searchConversationWindows, searchConversationWindowsByEmbedding, getActiveChannels, getReferencedMessageStats, upsertMessageVectorNative, deleteMessagesForGuild } from "./storeMessages.ts"; import { maybePruneActionLog, pruneActionLog, logAction, countActionsSince, getLastActionTime, getRecentActions, getRecentMemoryReflections, deleteReflectionRun, deleteMemoryReflectionRunsForGuild, getRecentBrowserSessions, indexResponseTriggersForAction, hasTriggeredResponse, hasReflectionBeenCompleted, markReflectionCompleted } from "./storeActionLog.ts"; import { wasLinkSharedSince, recordSharedLink } from "./storeLookups.ts"; import { getRecentVoiceSessions, getVoiceSessionEvents } from "./storeVoice.ts"; import { getReplyPerformanceStats, getStats } from "./storeStats.ts"; import { createAutomation, getAutomationById, countAutomations, listAutomations, getMostRecentAutomations, findAutomationsByQuery, setAutomationStatus, claimDueAutomations, finalizeAutomationRun, recordAutomationRun, getAutomationRuns } from "./storeAutomation.ts"; import { addMemoryFact, getFactProfileRows, getFactsForSubjectScoped, getFactsForSubjects, getFactsForScope, getFactsForSubjectsScoped, getMemoryFactById, getMemoryFactBySubjectAndFact, updateMemoryFact, deleteMemoryFact, deleteMemoryFactsForGuild, cleanupLegacyMemoryFacts, ensureSqliteVecReady, upsertMemoryFactVectorNative, getMemoryFactVectorNative, getMemoryFactVectorNativeScores, getMemorySubjects, archiveOldFactsForSubject, searchMemoryFactsLexical, searchMemoryFactsByEmbedding } from "./storeMemory.ts"; import { deleteSessionSummariesForGuild, getRecentSessionSummaries, pruneExpiredSessionSummaries, upsertSessionSummary } from "./storeSessionSummaries.ts";

export const SETTINGS_KEY = "runtime_settings"; const ACTION_LOG_RETENTION_DAYS_DEFAULT = 14; export const ACTION_LOG_RETENTION_DAYS_MIN = 1; export const ACTION_LOG_RETENTION_DAYS_MAX = 3650; const ACTION_LOG_MAX_ROWS_DEFAULT = 120_000; const ACTION_LOG_MAX_ROWS_MIN = 1000; export const ACTION_LOG_MAX_ROWS_RUNTIME_MIN = 1; export const ACTION_LOG_MAX_ROWS_MAX = 5_000_000; const ACTION_LOG_PRUNE_EVERY_WRITES_DEFAULT = 250; const ACTION_LOG_PRUNE_EVERY_WRITES_MIN = 1; const ACTION_LOG_PRUNE_EVERY_WRITES_MAX = 10_000;

function resolveEnvBoundedInt(rawValue, fallback, min, max) { const parsed = Math.floor(Number(rawValue)); if (!Number.isFinite(parsed)) return fallback; return clamp(parsed, min, max); }

function resolveStoreEnvInt(name, fallback, min, max) { return resolveEnvBoundedInt(process.env[name], fallback, min, max); }

type SqliteTableColumnRow = { name?: string; notnull?: number; };

const MEMORY_USER_FACT_TYPES = new Set(["preference", "profile", "relationship", "project", "other"]);

function hasMemoryFactsDualScopeSchema(db: Database) { const columns = db .prepare<SqliteTableColumnRow, []>("PRAGMA table_info(memory_facts)") .all(); if (!columns.length) return false;

const byName = new Map( columns.map((column) => [String(column?.name || "").trim().toLowerCase(), column]) ); const scopeColumn = byName.get("scope"); const userIdColumn = byName.get("user_id"); const guildIdColumn = byName.get("guild_id"); if (!scopeColumn || !userIdColumn || !guildIdColumn) return false; return Number(guildIdColumn.notnull || 0) === 0; }

function ensureMemoryFactsIndexes(db: Database) { db.exec(` DROP INDEX IF EXISTS idx_memory_scope_subject; DROP INDEX IF EXISTS idx_memory_scope_channel; DROP INDEX IF EXISTS idx_memory_scope_subject_type; DROP INDEX IF EXISTS idx_memory_scope_active;

CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_scope_unique
  ON memory_facts(scope, COALESCE(guild_id, ''), COALESCE(user_id, ''), subject, fact);
CREATE INDEX IF NOT EXISTS idx_memory_user_scope
  ON memory_facts(scope, user_id, subject, is_active, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_memory_guild_scope
  ON memory_facts(scope, guild_id, subject, is_active, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_memory_subject_active
  ON memory_facts(subject, is_active, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_memory_scope_subject_type
  ON memory_facts(scope, guild_id, subject, fact_type, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_memory_scope_channel
  ON memory_facts(scope, guild_id, channel_id, updated_at DESC);

`); }

function ensureMemoryFactsFtsSchema(db: Database) { db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS memory_facts_fts USING fts5( fact, evidence_text, subject, fact_type, content='memory_facts', content_rowid='id' );

CREATE TRIGGER IF NOT EXISTS memory_facts_ai AFTER INSERT ON memory_facts BEGIN
  INSERT INTO memory_facts_fts(rowid, fact, evidence_text, subject, fact_type)
  VALUES (new.id, new.fact, COALESCE(new.evidence_text, ''), new.subject, new.fact_type);
END;

CREATE TRIGGER IF NOT EXISTS memory_facts_ad AFTER DELETE ON memory_facts BEGIN
  INSERT INTO memory_facts_fts(memory_facts_fts, rowid, fact, evidence_text, subject, fact_type)
  VALUES ('delete', old.id, old.fact, COALESCE(old.evidence_text, ''), old.subject, old.fact_type);
END;

CREATE TRIGGER IF NOT EXISTS memory_facts_au AFTER UPDATE ON memory_facts BEGIN
  INSERT INTO memory_facts_fts(memory_facts_fts, rowid, fact, evidence_text, subject, fact_type)
  VALUES ('delete', old.id, old.fact, COALESCE(old.evidence_text, ''), old.subject, old.fact_type);
  INSERT INTO memory_facts_fts(rowid, fact, evidence_text, subject, fact_type)
  VALUES (new.id, new.fact, COALESCE(new.evidence_text, ''), new.subject, new.fact_type);
END;

`); // Use exec() not prepare().run() — on bun+Windows, a non-finalized prepared // statement at init time holds the WAL/SHM file handles open past db.close(), // which makes test temp-dir cleanup fail with EBUSY. db.exec("INSERT INTO memory_facts_fts(memory_facts_fts) VALUES('rebuild')"); }

function migrateMemoryFactsToDualScope(db: Database) { const userFactTypeList = [...MEMORY_USER_FACT_TYPES].map((value) => '${value}').join(", "); const migrateTx = db.transaction(() => { db.exec(` DROP INDEX IF EXISTS idx_memory_scope_subject; DROP INDEX IF EXISTS idx_memory_scope_channel; DROP INDEX IF EXISTS idx_memory_scope_subject_type; DROP INDEX IF EXISTS idx_memory_scope_active; DROP INDEX IF EXISTS idx_memory_scope_unique; DROP INDEX IF EXISTS idx_memory_user_scope; DROP INDEX IF EXISTS idx_memory_guild_scope; DROP INDEX IF EXISTS idx_memory_subject_active;

  CREATE TABLE memory_facts_new (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    scope TEXT NOT NULL DEFAULT 'guild',
    guild_id TEXT,
    channel_id TEXT,
    user_id TEXT,
    subject TEXT NOT NULL,
    fact TEXT NOT NULL,
    fact_type TEXT NOT NULL DEFAULT 'other',
    evidence_text TEXT,
    source_message_id TEXT,
    confidence REAL NOT NULL DEFAULT 0.5,
    is_active INTEGER NOT NULL DEFAULT 1
  );

  WITH classified AS (
    SELECT
      id,
      created_at,
      updated_at,
      guild_id,
      channel_id,
      subject,
      fact,
      fact_type,
      evidence_text,
      source_message_id,
      confidence,
      is_active,
      CASE
        WHEN subject = '__self__' THEN 'user'
        WHEN subject = '__lore__' THEN 'guild'
        WHEN LOWER(TRIM(fact_type)) IN ('guidance', 'behavioral') THEN 'guild'
        WHEN subject GLOB '[0-9]*'
          AND subject NOT GLOB '*[^0-9]*'
          AND LOWER(TRIM(fact_type)) IN (${userFactTypeList})
          THEN 'user'
        ELSE 'guild'
      END AS resolved_scope
    FROM memory_facts
  ),
  normalized AS (
    SELECT
      id,
      created_at,
      updated_at,
      resolved_scope AS scope,
      CASE WHEN resolved_scope = 'user' THEN NULL ELSE guild_id END AS guild_id,
      channel_id,
      CASE
        WHEN subject = '__self__' THEN NULL
        WHEN resolved_scope = 'user' THEN subject
        ELSE NULL
      END AS user_id,
      subject,
      fact,
      fact_type,
      evidence_text,
      source_message_id,
      confidence,
      is_active
    FROM classified
  ),
  ranked AS (
    SELECT
      id,
      created_at,
      updated_at,
      scope,
      guild_id,
      channel_id,
      user_id,
      subject,
      fact,
      fact_type,
      evidence_text,
      source_message_id,
      confidence,
      is_active,
      ROW_NUMBER() OVER (
        PARTITION BY
          scope,
          COALESCE(guild_id, ''),
          COALESCE(user_id, ''),
          subject,
          fact
        ORDER BY confidence DESC, updated_at DESC, id DESC
      ) AS row_num
    FROM normalized
  )
  INSERT INTO memory_facts_new(
    id,
    created_at,
    updated_at,
    scope,
    guild_id,
    channel_id,
    user_id,
    subject,
    fact,
    fact_type,
    evidence_text,
    source_message_id,
    confidence,
    is_active
  )
  SELECT
    id,
    created_at,
    updated_at,
    scope,
    guild_id,
    channel_id,
    user_id,
    subject,
    fact,
    fact_type,
    evidence_text,
    source_message_id,
    confidence,
    is_active
  FROM ranked
  WHERE row_num = 1;

  DROP TABLE memory_facts;
  ALTER TABLE memory_facts_new RENAME TO memory_facts;
`);

}); migrateTx(); }

function setupMemoryFactsSchema(db: Database) { if (!hasMemoryFactsDualScopeSchema(db)) { migrateMemoryFactsToDualScope(db); } ensureMemoryFactsIndexes(db); ensureMemoryFactsFtsSchema(db); }

export class Store { dbPath; db; sqliteVecReady; sqliteVecError; onActionLogged; actionLogRetentionDays; actionLogMaxRows; actionLogPruneEveryWrites; actionWritesSincePrune;

constructor(dbPath) { this.dbPath = dbPath; this.db = null; this.sqliteVecReady = null; this.sqliteVecError = ""; this.onActionLogged = null; this.actionLogRetentionDays = resolveStoreEnvInt( "ACTION_LOG_RETENTION_DAYS", ACTION_LOG_RETENTION_DAYS_DEFAULT, ACTION_LOG_RETENTION_DAYS_MIN, ACTION_LOG_RETENTION_DAYS_MAX ); this.actionLogMaxRows = resolveStoreEnvInt( "ACTION_LOG_MAX_ROWS", ACTION_LOG_MAX_ROWS_DEFAULT, ACTION_LOG_MAX_ROWS_MIN, ACTION_LOG_MAX_ROWS_MAX ); this.actionLogPruneEveryWrites = resolveStoreEnvInt( "ACTION_LOG_PRUNE_EVERY_WRITES", ACTION_LOG_PRUNE_EVERY_WRITES_DEFAULT, ACTION_LOG_PRUNE_EVERY_WRITES_MIN, ACTION_LOG_PRUNE_EVERY_WRITES_MAX ); this.actionWritesSincePrune = 0; }

init() { // SQLite in-memory databases (":memory:") and URI paths don't live on disk, // so skip the parent-dir mkdir. On bun+Windows, fs.mkdirSync(".", {recursive: true}) // throws EEXIST even with recursive:true, which would otherwise break in-memory tests. const isOnDiskPath = this.dbPath && this.dbPath !== ":memory:" && !this.dbPath.startsWith("file::memory:"); if (isOnDiskPath) { fs.mkdirSync(path.dirname(this.dbPath), { recursive: true }); } this.db = new Database(this.dbPath); this.db.exec("PRAGMA journal_mode = WAL;");

this.db.exec(`
  CREATE TABLE IF NOT EXISTS settings (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL,
    updated_at TEXT NOT NULL
  );

  CREATE TABLE IF NOT EXISTS messages (
    message_id TEXT PRIMARY KEY,
    created_at TEXT NOT NULL,
    guild_id TEXT,
    channel_id TEXT NOT NULL,
    author_id TEXT NOT NULL,
    author_name TEXT NOT NULL,
    is_bot INTEGER NOT NULL,
    content TEXT NOT NULL,
    referenced_message_id TEXT
  );

  CREATE TABLE IF NOT EXISTS actions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created_at TEXT NOT NULL,
    guild_id TEXT,
    channel_id TEXT,
    message_id TEXT,
    user_id TEXT,
    kind TEXT NOT NULL,
    content TEXT,
    metadata TEXT,
    usd_cost REAL NOT NULL DEFAULT 0
  );

  CREATE TABLE IF NOT EXISTS memory_facts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    scope TEXT NOT NULL DEFAULT 'guild',
    guild_id TEXT,
    channel_id TEXT,
    user_id TEXT,
    subject TEXT NOT NULL,
    fact TEXT NOT NULL,
    fact_type TEXT NOT NULL DEFAULT 'other',
    evidence_text TEXT,
    source_message_id TEXT,
    confidence REAL NOT NULL DEFAULT 0.5,
    is_active INTEGER NOT NULL DEFAULT 1
  );

  CREATE TABLE IF NOT EXISTS memory_fact_vectors_native (
    fact_id INTEGER NOT NULL,
    model TEXT NOT NULL,
    dims INTEGER NOT NULL,
    embedding_blob BLOB NOT NULL,
    updated_at TEXT NOT NULL,
    PRIMARY KEY (fact_id, model)
  );

  CREATE TABLE IF NOT EXISTS message_vectors_native (
    message_id TEXT NOT NULL,
    model TEXT NOT NULL,
    dims INTEGER NOT NULL,
    embedding_blob BLOB NOT NULL,
    updated_at TEXT NOT NULL,
    PRIMARY KEY (message_id, model)
  );

  CREATE TABLE IF NOT EXISTS shared_links (
    url TEXT PRIMARY KEY,
    first_shared_at TEXT NOT NULL,
    last_shared_at TEXT NOT NULL,
    share_count INTEGER NOT NULL DEFAULT 1,
    source TEXT
  );

  CREATE TABLE IF NOT EXISTS automations (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    guild_id TEXT NOT NULL,
    channel_id TEXT NOT NULL,
    created_by_user_id TEXT NOT NULL,
    created_by_name TEXT,
    title TEXT NOT NULL,
    instruction TEXT NOT NULL,
    schedule_json TEXT NOT NULL,
    next_run_at TEXT,
    status TEXT NOT NULL DEFAULT 'active',
    is_running INTEGER NOT NULL DEFAULT 0,
    running_started_at TEXT,
    last_run_at TEXT,
    last_error TEXT,
    last_result TEXT,
    match_text TEXT NOT NULL
  );

  CREATE TABLE IF NOT EXISTS automation_runs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    automation_id INTEGER NOT NULL,
    created_at TEXT NOT NULL,
    started_at TEXT NOT NULL,
    finished_at TEXT,
    status TEXT NOT NULL,
    summary TEXT,
    error TEXT,
    message_id TEXT,
    metadata TEXT
  );

  CREATE TABLE IF NOT EXISTS response_triggers (
    trigger_message_id TEXT PRIMARY KEY,
    action_id INTEGER NOT NULL,
    created_at TEXT NOT NULL
  );

  CREATE TABLE IF NOT EXISTS reflection_checkpoints (
    date_key TEXT NOT NULL,
    guild_id TEXT NOT NULL,
    completed_at TEXT NOT NULL,
    run_id TEXT,
    PRIMARY KEY (date_key, guild_id)
  );

  CREATE TABLE IF NOT EXISTS session_summaries (
    session_id TEXT PRIMARY KEY,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    guild_id TEXT NOT NULL,
    channel_id TEXT NOT NULL,
    modality TEXT NOT NULL DEFAULT 'voice',
    started_at TEXT,
    ended_at TEXT NOT NULL,
    summary_text TEXT NOT NULL
  );

  CREATE INDEX IF NOT EXISTS idx_messages_channel_time ON messages(channel_id, created_at DESC);
  CREATE INDEX IF NOT EXISTS idx_messages_guild_time ON messages(guild_id, created_at DESC);
  CREATE INDEX IF NOT EXISTS idx_actions_kind_time ON actions(kind, created_at DESC);
  CREATE INDEX IF NOT EXISTS idx_actions_time ON actions(created_at DESC);
  CREATE INDEX IF NOT EXISTS idx_memory_vectors_native_model_dims ON memory_fact_vectors_native(model, dims);
  CREATE INDEX IF NOT EXISTS idx_message_vectors_native_model_dims ON message_vectors_native(model, dims);
  CREATE INDEX IF NOT EXISTS idx_shared_links_last_shared_at ON shared_links(last_shared_at DESC);
  CREATE INDEX IF NOT EXISTS idx_automations_scope_status_next ON automations(guild_id, status, next_run_at);
  CREATE INDEX IF NOT EXISTS idx_automations_running_next ON automations(is_running, next_run_at);
  CREATE INDEX IF NOT EXISTS idx_automations_match_text ON automations(guild_id, match_text);
  CREATE INDEX IF NOT EXISTS idx_automation_runs_job_time ON automation_runs(automation_id, created_at DESC);
  CREATE INDEX IF NOT EXISTS idx_response_triggers_action_id ON response_triggers(action_id);
  CREATE INDEX IF NOT EXISTS idx_reflection_checkpoints_completed_at ON reflection_checkpoints(completed_at DESC);
  CREATE INDEX IF NOT EXISTS idx_session_summaries_scope_time ON session_summaries(guild_id, channel_id, modality, ended_at DESC);
`);
this.ensureSqliteVecReady();
setupMemoryFactsSchema(this.db);
cleanupLegacyMemoryFacts(this);
pruneExpiredSessionSummaries(this);

if (!this.db.prepare("SELECT 1 FROM settings WHERE key = ?").get(SETTINGS_KEY)) {
  const defaultSettings = minimizeSettingsIntent({});
  this.db
    .prepare("INSERT INTO settings(key, value, updated_at) VALUES(?, ?, ?)")
    .run(SETTINGS_KEY, JSON.stringify(defaultSettings), nowIso());
} else {
  const row = this.db.prepare("SELECT value FROM settings WHERE key = ?").get(SETTINGS_KEY);
  this.rewriteRuntimeSettingsRow(row?.value);
}

this.pruneActionLog({ now: nowIso() });

}

close() { if (this.db) { // Finalize any dangling prepared statements before closing. On bun+Windows, // prepared statements are not finalized on db.close() automatically, and // their open handles keep the SQLite WAL/SHM files locked, causing EBUSY // when callers (mostly tests) try to fs.rm the containing directory. // Running GC before close releases the statements synchronously; the // second GC call after close handles anything the close itself released. // No-op on runtimes without Bun.gc. const runGc = typeof Bun !== "undefined" && typeof Bun.gc === "function"; if (runGc) Bun.gc(true); this.db.close(); this.db = null; if (runGc) Bun.gc(true); } }

rewriteRuntimeSettingsRow(rawValue) { return rewriteRuntimeSettingsRow(this, rawValue); }

getSettings() { return getSettings(this); }

getSettingsRecord() { return getSettingsRecord(this); }

setSettings(next) { return setSettings(this, next); }

patchSettings(patch) { return patchSettings(this, patch); }

patchSettingsWithVersion(patch, expectedUpdatedAt) { return patchSettingsWithVersion(this, patch, expectedUpdatedAt); }

replaceSettingsWithVersion(next, expectedUpdatedAt) { return replaceSettingsWithVersion(this, next, expectedUpdatedAt); }

resetSettings() { return resetSettings(this); }

recordMessage(message) { return recordMessage(this, message); }

getRecentMessages(channelId, limit = 40) { return getRecentMessages(this, channelId, limit); }

getRecentMessagesAcrossGuild(guildId, limit = 120) { return getRecentMessagesAcrossGuild(this, guildId, limit); }

deleteMessagesForGuild(guildId: string) { return deleteMessagesForGuild(this, guildId); }

getMessagesInWindow(opts: { guildId: string; channelId?: string | null; sinceIso?: string | null; untilIso?: string | null; limit?: number; }) { return getMessagesInWindow(this, opts); }

searchRelevantMessages(channelId, queryText, limit = 8) { return searchRelevantMessages(this, channelId, queryText, limit); }

searchConversationWindows(opts: { guildId?: string | null; channelId?: string | null; queryText?: string; limit?: number; maxAgeHours?: number; before?: number; after?: number; }) { return searchConversationWindows(this, opts); }

searchConversationWindowsByEmbedding(opts: { guildId?: string | null; channelId?: string | null; queryEmbedding: number[]; model: string; limit?: number; maxAgeHours?: number; before?: number; after?: number; }) { return searchConversationWindowsByEmbedding(this, opts); }

upsertMessageVectorNative(opts: { messageId: string; model: string; embedding: number[]; updatedAt?: string; }) { return upsertMessageVectorNative(this, opts); }

getActiveChannels(guildId, hours = 24, limit = 10) { return getActiveChannels(this, guildId, hours, limit); }

getReferencedMessageStats({ messageIds, guildId = null, sinceIso = null }: { messageIds: string[]; guildId?: string | null; sinceIso?: string | null; }) { return getReferencedMessageStats(this, { messageIds, guildId, sinceIso }); }

maybePruneActionLog(opts: { now?: string } = {}) { return maybePruneActionLog(this, opts); }

pruneActionLog(opts: { now?: string; maxAgeDays?: number; maxRows?: number } = {}) { return pruneActionLog(this, opts); }

logAction(action) { return logAction(this, action); }

countActionsSince(kind, sinceIso) { return countActionsSince(this, kind, sinceIso); }

getLastActionTime(kind) { return getLastActionTime(this, kind); }

getRecentActions(limit = 200, opts: { kinds?: string[]; sinceIso?: string | null; guildId?: string | null } = {}) { return getRecentActions(this, limit, opts); }

getRecentMemoryReflections(limit = 20, opts: { guildId?: string | null } = {}) { return getRecentMemoryReflections(this, limit, opts); }

getRecentBrowserSessions(limit = 50, opts: { sinceIso?: string | null; guildId?: string | null } = {}) { return getRecentBrowserSessions(this, limit, opts); }

indexResponseTriggersForAction(opts: { actionId; kind; metadata; createdAt?: string; }) { return indexResponseTriggersForAction(this, opts); }

hasTriggeredResponse(triggerMessageId) { return hasTriggeredResponse(this, triggerMessageId); }

hasReflectionBeenCompleted(dateKey: string, guildId: string): boolean { return hasReflectionBeenCompleted(this, dateKey, guildId); }

markReflectionCompleted(dateKey: string, guildId: string, opts: { runId?: string | null; completedAt?: string | null } = {}) { return markReflectionCompleted(this, dateKey, guildId, opts); }

deleteReflectionRun(runId: string): { deleted: number } { return deleteReflectionRun(this, runId); }

deleteMemoryReflectionRunsForGuild(guildId: string) { return deleteMemoryReflectionRunsForGuild(this, guildId); }

wasLinkSharedSince(url, sinceIso) { return wasLinkSharedSince(this, url, sinceIso); }

recordSharedLink(opts: { url; source? }) { return recordSharedLink(this, opts); }

getRecentVoiceSessions(limit = 3, opts: { sinceIso?: string | null; guildId?: string | null } = {}) { return getRecentVoiceSessions(this, limit, opts); }

getVoiceSessionEvents(sessionId: string, limit = 500) { return getVoiceSessionEvents(this, sessionId, limit); }

getReplyPerformanceStats(opts: { windowHours?: number; maxSamples?: number; guildId?: string | null } = {}) { return getReplyPerformanceStats(this, opts); }

getStats(opts: { guildId?: string | null } = {}) { return getStats(this, opts); }

createAutomation(opts: { guildId; channelId; createdByUserId; createdByName?; title; instruction; schedule; nextRunAt?; }) { return createAutomation(this, opts); }

getAutomationById(automationId, guildId = null) { return getAutomationById(this, automationId, guildId); }

countAutomations(opts: { guildId; statuses? }) { return countAutomations(this, opts); }

listAutomations(opts: { guildId; channelId?; statuses?; query?; limit?; }) { return listAutomations(this, opts); }

getMostRecentAutomations(opts: { guildId; channelId?; statuses?; limit?; }) { return getMostRecentAutomations(this, opts); }

findAutomationsByQuery(opts: { guildId; channelId?; query?; statuses?; limit?; }) { return findAutomationsByQuery(this, opts); }

setAutomationStatus(opts: { automationId; guildId; status; nextRunAt?; lastError?; lastResult?; }) { return setAutomationStatus(this, opts); }

claimDueAutomations(opts: { now?: string; limit?: number } = {}) { return claimDueAutomations(this, opts); }

finalizeAutomationRun(opts: { automationId?: number | string; guildId?: string; status?: string; nextRunAt?: string | null; lastRunAt?: string | null; lastError?: string | null; lastResult?: string | null; } = {}) { return finalizeAutomationRun(this, opts); }

recordAutomationRun(opts: { automationId; startedAt?; finishedAt?; status?; summary?; error?; messageId?; metadata?; }) { return recordAutomationRun(this, opts); }

getAutomationRuns(opts: { automationId?: number | string; guildId?: string; limit?: number; } = {}) { return getAutomationRuns(this, opts); }

addMemoryFact(fact) { return addMemoryFact(this, fact); }

getFactsForSubjectScoped(subject, limit = 12, scope = null) { return getFactsForSubjectScoped(this, subject, limit, scope); }

getFactsForSubjects(subjects, limit = 80, scope = null) { return getFactsForSubjects(this, subjects, limit, scope); }

getFactProfileRows(opts: { guildId?; scope?: "user" | "guild" | "owner" | null; subjects?; limit? } = {}) { return getFactProfileRows(this, opts); }

getFactsForScope(opts: { guildId?; scope?: "user" | "guild" | "owner" | null; limit?; subjectIds?; factTypes?; includePortableUserScope?: boolean; includeOwnerScope?: boolean; queryText?; }) { return getFactsForScope(this, opts); }

searchMemoryFactsLexical(opts: { guildId?; scope?: "user" | "guild" | "owner" | null; subjectIds?; factTypes?; queryText?; queryTokens?; limit?; }) { return searchMemoryFactsLexical(this, opts); }

searchMemoryFactsByEmbedding(opts: { guildId?; scope?: "user" | "guild" | "owner" | null; subjectIds?; factTypes?; model; queryEmbedding; limit?; }) { return searchMemoryFactsByEmbedding(this, opts); }

getFactsForSubjectsScoped(opts: { guildId?; scope?: "user" | "guild" | "owner" | null; subjectIds?; perSubjectLimit?; totalLimit?; } = {}) { return getFactsForSubjectsScoped(this, opts); }

getMemoryFactBySubjectAndFact(opts: { guildId?: string | null; scope?: "user" | "guild" | "owner" | null; userId?: string | null; subject: string; fact: string; }) { return getMemoryFactBySubjectAndFact(this, opts); }

getMemoryFactById(factId, guildId = null, scope: "user" | "guild" | "owner" | null = null) { return getMemoryFactById(this, factId, guildId, scope); }

updateMemoryFact(opts: { guildId?; scope?: "user" | "guild" | "owner" | null; userId?; factId; subject; fact; factType?; evidenceText?; confidence?; }) { return updateMemoryFact(this, opts); }

deleteMemoryFact(opts: { guildId?; scope?: "user" | "guild" | "owner" | null; userId?; factId; }) { return deleteMemoryFact(this, opts); }

deleteMemoryFactsForGuild(guildId: string) { return deleteMemoryFactsForGuild(this, guildId); }

upsertSessionSummary(opts: { sessionId: string; guildId: string; channelId: string; summaryText: string; startedAt?: string | null; endedAt?: string | null; modality?: string; }) { return upsertSessionSummary(this, opts); }

getRecentSessionSummaries(opts: { guildId: string; channelId?: string | null; modality?: string; sinceIso?: string | null; beforeIso?: string | null; limit?: number; }) { return getRecentSessionSummaries(this, opts); }

deleteSessionSummariesForGuild(guildId: string) { return deleteSessionSummariesForGuild(this, guildId); }

pruneExpiredSessionSummaries(opts: { retentionHours?: number } = {}) { return pruneExpiredSessionSummaries(this, opts); }

ensureSqliteVecReady() { return ensureSqliteVecReady(this); }

upsertMemoryFactVectorNative(opts: { factId; model; embedding; updatedAt? }) { return upsertMemoryFactVectorNative(this, opts); }

getMemoryFactVectorNative(factId, model) { return getMemoryFactVectorNative(this, factId, model); }

getMemoryFactVectorNativeScores(opts: { factIds; model; queryEmbedding }) { return getMemoryFactVectorNativeScores(this, opts); }

getMemorySubjects(limit = 80, scope: { guildId?: string | null; scope?: "user" | "guild" | "owner" | null; includePortableUserScope?: boolean; includeOwnerScope?: boolean } | null = null) { return getMemorySubjects(this, limit, scope); }

archiveOldFactsForSubject(opts: { guildId?; scope?: "user" | "guild" | "owner" | null; userId?; subject; factType?; keep?; }) { return archiveOldFactsForSubject(this, opts); } }