This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

niri / src / db.ts
9.2 kB 272 lines
1import Database from "better-sqlite3" 2import fs from "fs" 3import path from "path" 4import * as sqliteVec from "sqlite-vec" 5import { HOME_DIR } from "./container/config" 6 7const DB_PATH = path.join(HOME_DIR, "niri.db") 8export const MEMORY_EMBEDDING_DIMENSIONS = 3072 9 10let db: Database.Database 11let vecAvailable = false 12 13function ensureWritableDirOrThrow(dirPath: string, purpose: string): void { 14 try { 15 fs.mkdirSync(dirPath, { recursive: true }) 16 fs.accessSync(dirPath, fs.constants.W_OK) 17 } catch (err: any) { 18 let owner = "unknown" 19 try { 20 const st = fs.statSync(dirPath) 21 owner = `${st.uid}:${st.gid}` 22 } catch { 23 // ignore 24 } 25 26 const uid = typeof process.getuid === "function" ? process.getuid() : undefined 27 const gid = typeof process.getgid === "function" ? process.getgid() : undefined 28 const who = uid !== undefined && gid !== undefined ? `${uid}:${gid}` : "current user" 29 30 throw new Error( 31 [ 32 `[db] cannot write ${purpose} under ${dirPath}`, 33 `- dir owner: ${owner}`, 34 `- process uid:gid: ${who}`, 35 "", 36 "Fix:", 37 "- If running locally: `sudo chown -R $(id -u):$(id -g) home`", 38 "- If running via docker-compose: set `AGENT_UID`/`AGENT_GID` in .env to match `id -u`/`id -g`, then recreate the container", 39 "", 40 `Original error: ${err?.message ?? String(err)}`, 41 ].join("\n"), 42 ) 43 } 44} 45 46export function initDb(): void { 47 ensureWritableDirOrThrow(HOME_DIR, "niri.db") 48 db = new Database(DB_PATH) 49 50 db.pragma("journal_mode = WAL") 51 db.pragma("foreign_keys = ON") 52 try { 53 sqliteVec.load(db) 54 vecAvailable = true 55 } catch (err: any) { 56 vecAvailable = false 57 console.warn(`[db] sqlite-vec unavailable: ${err?.message ?? String(err)}`) 58 } 59 60 db.exec(` 61 create table if not exists conversations ( 62 id integer primary key autoincrement, 63 startedAt text not null, 64 source text not null, 65 tokens integer not null default 0 66 ); 67 68 create table if not exists messages ( 69 id integer primary key autoincrement, 70 convId integer not null references conversations(id), 71 role text not null, 72 content text not null, 73 toolCalls text, -- json blob, null if none 74 toolCallId text, -- for role=tool responses 75 createdAt text not null default (datetime('now')) 76 ); 77 78 create table if not exists discord_messages ( 79 message_id text primary key, 80 channel_id text not null, 81 guild_id text, 82 channel_type integer, 83 author_id text, 84 author_username text, 85 content text not null default '', 86 created_at text not null, 87 is_dm integer not null default 0, 88 mentions_bot integer not null default 0, 89 is_from_bot integer not null default 0, 90 first_seen_at text not null, 91 last_seen_at text not null, 92 raw_json text not null 93 ); 94 95 create index if not exists idx_discord_messages_channel 96 on discord_messages(channel_id, message_id desc); 97 create index if not exists idx_discord_messages_created 98 on discord_messages(created_at desc); 99 100 create table if not exists discord_items ( 101 item_id text primary key, 102 message_id text not null references discord_messages(message_id) on delete cascade, 103 bucket text not null, 104 status text not null default 'pending', 105 action_taken text not null default 'none', 106 decision_note text, 107 first_seen_at text not null, 108 last_seen_at text not null, 109 last_decision_at text 110 ); 111 112 create index if not exists idx_discord_items_status 113 on discord_items(status, last_seen_at desc); 114 create index if not exists idx_discord_items_message 115 on discord_items(message_id); 116 117 create table if not exists discord_channels ( 118 channel_id text primary key, 119 guild_id text, 120 channel_type integer, 121 channel_name text, 122 guild_name text, 123 topic text, 124 is_dm integer not null default 0, 125 configured integer not null default 0, 126 note text, 127 last_note_at text, 128 first_seen_at text not null, 129 last_seen_at text not null, 130 raw_json text not null 131 ); 132 133 create index if not exists idx_discord_channels_configured 134 on discord_channels(configured, guild_name, channel_name); 135 create index if not exists idx_discord_channels_last_seen 136 on discord_channels(last_seen_at desc); 137 138 create table if not exists discord_meta ( 139 key text primary key, 140 value text not null, 141 updated_at text not null 142 ); 143 144 create table if not exists memory_documents ( 145 id integer primary key autoincrement, 146 path text not null unique, 147 kind text not null, 148 title text not null, 149 mtime_ms integer not null, 150 content_hash text not null, 151 updated_at text not null default (datetime('now')) 152 ); 153 154 create index if not exists idx_memory_documents_kind 155 on memory_documents(kind, path); 156 157 create table if not exists memory_chunks ( 158 id integer primary key autoincrement, 159 document_id integer not null references memory_documents(id) on delete cascade, 160 chunk_index integer not null, 161 title text not null, 162 heading_path text, 163 chunk_text text not null, 164 tags text, 165 created_at text not null default (datetime('now')), 166 unique(document_id, chunk_index) 167 ); 168 169 create index if not exists idx_memory_chunks_document 170 on memory_chunks(document_id, chunk_index); 171 172 create virtual table if not exists memory_chunks_fts using fts5( 173 title, 174 heading_path, 175 chunk_text, 176 tags, 177 content='memory_chunks', 178 content_rowid='id', 179 tokenize='porter unicode61' 180 ); 181 182 create trigger if not exists memory_chunks_ai after insert on memory_chunks begin 183 insert into memory_chunks_fts(rowid, title, heading_path, chunk_text, tags) 184 values (new.id, new.title, new.heading_path, new.chunk_text, new.tags); 185 end; 186 187 create trigger if not exists memory_chunks_ad after delete on memory_chunks begin 188 insert into memory_chunks_fts(memory_chunks_fts, rowid, title, heading_path, chunk_text, tags) 189 values ('delete', old.id, old.title, old.heading_path, old.chunk_text, old.tags); 190 end; 191 192 create trigger if not exists memory_chunks_au after update on memory_chunks begin 193 insert into memory_chunks_fts(memory_chunks_fts, rowid, title, heading_path, chunk_text, tags) 194 values ('delete', old.id, old.title, old.heading_path, old.chunk_text, old.tags); 195 insert into memory_chunks_fts(rowid, title, heading_path, chunk_text, tags) 196 values (new.id, new.title, new.heading_path, new.chunk_text, new.tags); 197 end; 198 199 create table if not exists memory_embedding_meta ( 200 chunk_id integer primary key references memory_chunks(id) on delete cascade, 201 model text not null, 202 dimensions integer not null, 203 content_hash text not null, 204 updated_at text not null default (datetime('now')) 205 ); 206 207 create index if not exists idx_memory_embedding_meta_model 208 on memory_embedding_meta(model, dimensions); 209 210 create table if not exists memory_embedding_prototypes ( 211 id integer primary key, 212 name text not null unique, 213 category text not null, 214 model text not null, 215 dimensions integer not null, 216 content_hash text not null, 217 updated_at text not null default (datetime('now')) 218 ); 219 `) 220 221 if (vecAvailable) { 222 db.exec(` 223 create virtual table if not exists memory_chunk_vec using vec0( 224 embedding float[${MEMORY_EMBEDDING_DIMENSIONS}] distance_metric=cosine 225 ); 226 227 create virtual table if not exists memory_prototype_vec using vec0( 228 embedding float[${MEMORY_EMBEDDING_DIMENSIONS}] distance_metric=cosine 229 ); 230 `) 231 } 232 233 console.log("[db] ready") 234} 235 236export function startConversation(source: string, startedAt: string): number { 237 const stmt = db.prepare("insert into conversations (startedAt, source) values (?, ?)") 238 const result = stmt.run(startedAt, source) 239 return result.lastInsertRowid as number 240} 241 242export function logMessage( 243 convId: number, 244 role: string, 245 content: string, 246 toolCalls?: unknown, 247 toolCallId?: string, 248): void { 249 const stmt = db.prepare( 250 "insert into messages (convId, role, content, toolCalls, toolCallId) values (?, ?, ?, ?, ?)", 251 ) 252 stmt.run( 253 convId, 254 role, 255 content, 256 toolCalls ? JSON.stringify(toolCalls) : null, 257 toolCallId ?? null, 258 ) 259} 260 261export function endConversation(id: number, tokens: number): void { 262 db.prepare("update conversations set tokens = ? where id = ?").run(tokens, id) 263} 264 265export function getDb(): Database.Database { 266 if (!db) throw new Error("Database not initialized") 267 return db 268} 269 270export function isVecAvailable(): boolean { 271 return vecAvailable 272}