my harness for niri
1
fork

Configure Feed

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

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