@hera-al/server 1.6.41 → 1.6.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/agent/agent-service.js +1 -1
  2. package/dist/agent/prompt-builder.d.ts +1 -1
  3. package/dist/agent/prompt-builder.js +1 -1
  4. package/dist/agent/session-agent.d.ts +1 -1
  5. package/dist/agent/session-agent.js +1 -1
  6. package/dist/agent/workspace-files.js +1 -1
  7. package/dist/config.d.ts +21 -0
  8. package/dist/config.js +1 -1
  9. package/dist/gateway/bridge.d.ts +13 -3
  10. package/dist/gateway/channel-manager.d.ts +3 -9
  11. package/dist/gateway/channel-manager.js +1 -1
  12. package/dist/gateway/channels/mesh.d.ts +14 -0
  13. package/dist/gateway/channels/mesh.js +1 -1
  14. package/dist/gateway/channels/telegram/index.d.ts +22 -12
  15. package/dist/gateway/channels/telegram/index.js +1 -1
  16. package/dist/gateway/channels/webchat.d.ts +3 -6
  17. package/dist/gateway/channels/webchat.js +1 -1
  18. package/dist/gateway/channels/whatsapp.d.ts +3 -9
  19. package/dist/gateway/channels/whatsapp.js +1 -1
  20. package/dist/gateway/typing-coordinator.d.ts +43 -0
  21. package/dist/gateway/typing-coordinator.js +1 -0
  22. package/dist/memory/concept-store.d.ts +27 -0
  23. package/dist/memory/concept-store.js +1 -1
  24. package/dist/memory/memory-manager.d.ts +6 -1
  25. package/dist/memory/memory-manager.js +1 -1
  26. package/dist/memory/memory-search.d.ts +63 -0
  27. package/dist/memory/memory-search.js +1 -1
  28. package/dist/server.d.ts +3 -0
  29. package/dist/server.js +1 -1
  30. package/dist/tools/cron-tools.js +1 -1
  31. package/dist/tools/memory-tools.js +1 -1
  32. package/dist/tools/operational-context-tools.d.ts +20 -0
  33. package/dist/tools/operational-context-tools.js +1 -0
  34. package/dist/tools/server-tools.d.ts +11 -1
  35. package/dist/tools/server-tools.js +1 -1
  36. package/installationPkg/AGENTS.md +4 -0
  37. package/installationPkg/BEHAVIOUR.md +95 -76
  38. package/installationPkg/config.example.yaml +31 -3
  39. package/package.json +1 -1
@@ -1 +1 @@
1
- import{existsSync as e,readdirSync as t,readFileSync as n,statSync as s,copyFileSync as i,renameSync as r,unlinkSync as h,watch as a}from"node:fs";import{join as o,relative as c,basename as d,dirname as l}from"node:path";import m from"better-sqlite3";import p from"openai";import u from"hnswlib-node";const{HierarchicalNSW:E}=u;import{createLogger as b}from"../utils/logger.js";import{L0Generator as f,L0_DEFAULT as g}from"./l0-generator.js";const T=b("MemorySearch"),x="cosine";export class MemorySearch{memoryDir;dataDir;opts;indexDb=null;indexHnsw=null;watcher=null;debounceTimer=null;embedTimer=null;indexing=!1;embedding=!1;stopped=!0;l0Generator;searchDb=null;searchHnsw=null;openai=null;indexDbPath;searchDbPath;searchNextDbPath;indexHnswPath;searchHnswPath;searchNextHnswPath;constructor(e,t,n){this.memoryDir=e,this.dataDir=t,this.opts=n,this.indexDbPath=o(t,"memory-index.db"),this.searchDbPath=o(t,"memory-search.db"),this.searchNextDbPath=o(t,"memory-search-next.db"),this.indexHnswPath=o(t,"memory-vectors.hnsw"),this.searchHnswPath=o(t,"memory-vectors-search.hnsw"),this.searchNextHnswPath=o(t,"memory-vectors-search-next.hnsw"),this.isOpenAI()&&(this.openai=new p({apiKey:n.apiKey,...n.baseURL?{baseURL:n.baseURL}:{}})),this.l0Generator=new f(n.l0??g)}isOpenAI(){return(this.opts.baseURL||"https://api.openai.com/v1").includes("openai.com")}getMaxInjectedChars(){return this.opts.maxInjectedChars}async start(){T.info("Starting memory search engine..."),this.stopped=!1,this.indexDb=new m(this.indexDbPath),this.indexDb.pragma("journal_mode = WAL"),this.migrateEmbeddingsTable(),this.indexDb.exec("\n CREATE TABLE IF NOT EXISTS documents (\n path TEXT PRIMARY KEY,\n mtime_ms INTEGER NOT NULL,\n size INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS chunks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n doc_path TEXT NOT NULL,\n chunk_idx INTEGER NOT NULL,\n role TEXT NOT NULL DEFAULT '',\n timestamp TEXT NOT NULL DEFAULT '',\n session_key TEXT NOT NULL DEFAULT '',\n content TEXT NOT NULL,\n UNIQUE(doc_path, chunk_idx)\n );\n\n CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(\n content,\n content='chunks',\n content_rowid='id',\n tokenize='porter unicode61'\n );\n\n CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN\n INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\n CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN\n INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', old.id, old.content);\n END;\n\n CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN\n INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', old.id, old.content);\n INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\n -- Lookup table only (no vector BLOB) — vectors live in HNSW index\n CREATE TABLE IF NOT EXISTS embeddings (\n chunk_id INTEGER PRIMARY KEY\n );\n\n -- Metadata for detecting config changes (model, dimensions)\n CREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n\n -- Utility scoring: tracks how often each chunk is retrieved and used\n CREATE TABLE IF NOT EXISTS chunk_utility (\n chunk_id INTEGER PRIMARY KEY,\n access_count INTEGER DEFAULT 0,\n last_accessed TEXT,\n first_accessed TEXT\n );\n"),this.checkEmbeddingConfigChange(),this.initIndexHnsw(),await this.indexFiles(),await this.embedPending(),this.indexDb&&await this.l0Generator.start(this.indexDb),this.publishSnapshot(),this.maybeSwap(),this.startWatcher(),this.opts.embedIntervalMs>0&&(this.embedTimer=setInterval(()=>{this.embedPending().catch(e=>T.error(`Embed cycle error: ${e}`))},this.opts.embedIntervalMs)),T.info("Memory search engine started")}stop(){this.stopped=!0,this.watcher&&(this.watcher.close(),this.watcher=null),this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.embedTimer&&(clearInterval(this.embedTimer),this.embedTimer=null),this.indexDb&&(this.indexDb.close(),this.indexDb=null),this.searchDb&&(this.searchDb.close(),this.searchDb=null),this.indexHnsw=null,this.searchHnsw=null,this.l0Generator.stop(),T.info("Memory search engine stopped")}migrateEmbeddingsTable(){if(!this.indexDb)return;if(this.indexDb.prepare("PRAGMA table_info(embeddings)").all().some(e=>"vector"===e.name)){T.info("Migrating: dropping old embeddings table (had vector BLOB). All embeddings will be re-created via HNSW."),this.indexDb.exec("DROP TABLE IF EXISTS embeddings");try{h(this.indexHnswPath)}catch{}}}checkEmbeddingConfigChange(){if(!this.indexDb)return;const e=this.indexDb.prepare("SELECT value FROM meta WHERE key = ?"),t=this.indexDb.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)"),n=e.get("embedding_model")?.value,s=e.get("embedding_dimensions")?.value,i=this.opts.embeddingModel,r=String(this.opts.embeddingDimensions),a=void 0!==n&&n!==i,o=void 0!==s&&s!==r;if(a||o){const e=[];a&&e.push(`model: ${n} → ${i}`),o&&e.push(`dimensions: ${s} → ${r}`),T.info(`Embedding config changed (${e.join(", ")}). Wiping embeddings + HNSW for full re-embed.`),this.indexDb.exec("DELETE FROM embeddings");try{h(this.indexHnswPath)}catch{}}t.run("embedding_model",i),t.run("embedding_dimensions",r)}initIndexHnsw(){const t=this.opts.embeddingDimensions;if(this.indexHnsw=new E(x,t),e(this.indexHnswPath))try{this.indexHnsw.readIndexSync(this.indexHnswPath,!0),T.info(`Loaded HNSW index: ${this.indexHnsw.getCurrentCount()} points`)}catch(e){T.warn(`Failed to load HNSW index, creating new: ${e}`),this.indexHnsw.initIndex({maxElements:1e4,m:16,efConstruction:200,allowReplaceDeleted:!0})}else this.indexHnsw.initIndex({maxElements:1e4,m:16,efConstruction:200,allowReplaceDeleted:!0}),T.info("Created new HNSW index")}ensureHnswCapacity(e){if(!this.indexHnsw)return;const t=this.indexHnsw.getMaxElements();if(this.indexHnsw.getCurrentCount()+e>t){const n=Math.max(2*t,this.indexHnsw.getCurrentCount()+e+1e3);this.indexHnsw.resizeIndex(n),T.info(`Resized HNSW index: ${t} → ${n}`)}}async search(e,t){const n=t??this.opts.maxResults;if(this.maybeSwap(),!this.searchDb)return T.warn("Search DB not available"),[];const s=this.bm25Search(e,20);let i=[];try{const t=await this.embedText(e);t&&this.searchHnsw&&this.searchHnsw.getCurrentCount()>0&&(i=this.denseSearch(t,20))}catch(e){T.warn(`Dense search failed, using BM25 only: ${e}`)}const r=function(e,t,n){const s=new Map;for(let t=0;t<e.length;t++){const{id:i}=e[t];s.set(i,(s.get(i)??0)+1/(n+t+1))}for(let e=0;e<t.length;e++){const{id:i}=t[e];s.set(i,(s.get(i)??0)+1/(n+e+1))}const i=Array.from(s.entries()).map(([e,t])=>({id:e,score:t})).sort((e,t)=>t.score-e.score);return i}(s,i,this.opts.rrfK),h=Math.min(r.length,40),a=this.searchDb.prepare("SELECT doc_path, role, timestamp, session_key, content, l0 FROM chunks WHERE id = ?"),o=[];for(let e=0;e<h;e++){const{id:t,score:n}=r[e],s=a.get(t);s&&o.push({id:t,score:n,row:s})}const c=this.getUtilityScores(o.map(e=>e.id)),d=Date.now();for(const e of o){const t=c.get(e.id),n=t?.access_count??0;if(n>0&&(e.score+=.05*Math.log(n+1)),0===n&&e.row.timestamp){const t=new Date(e.row.timestamp).getTime();if(!isNaN(t)){const n=(d-t)/864e5;n>30&&(e.score-=Math.min(.001*(n-30),.05))}}}o.sort((e,t)=>t.score-e.score);const l=function(e,t,n){if(e.length<=t)return e;const s=e.map(e=>function(e){const t=new Set;for(const n of e.toLowerCase().matchAll(/\b\w{2,}\b/g))t.add(n[0]);return t}(e.row.content)),i=e[0]?.score??1,r=e[e.length-1]?.score??0,h=i-r||1,a=[],o=new Set(e.map((e,t)=>t));a.push(0),o.delete(0);for(;a.length<t&&o.size>0;){let t=-1,i=-1/0;for(const c of o){const o=(e[c].score-r)/h;let d=0;for(const e of a){const t=y(s[c],s[e]);t>d&&(d=t)}const l=n*o-(1-n)*d;l>i&&(i=l,t=c)}if(t<0)break;a.push(t),o.delete(t)}return a.map(t=>e[t])}(o,n,.7),m=[];for(const{id:e,score:t,row:n}of l){const s=n.l0&&this.l0Generator.enabled?n.l0:n.content.length>this.opts.maxSnippetChars?n.content.slice(0,this.opts.maxSnippetChars)+"...":n.content;m.push({chunkId:e,path:n.doc_path,sessionKey:n.session_key,snippet:s,score:t,role:n.role,timestamp:n.timestamp})}return T.info(`Search "${e.slice(0,60)}": ${m.length} results (sparse=${s.length}, dense=${i.length}, candidates=${o.length}, mmr=${l.length})`),m}recordAccess(e){if(!this.indexDb||0===e.length)return;const t=(new Date).toISOString(),n=this.indexDb.prepare("\n INSERT INTO chunk_utility (chunk_id, access_count, last_accessed, first_accessed)\n VALUES (?, 1, ?, ?)\n ON CONFLICT(chunk_id) DO UPDATE SET\n access_count = access_count + 1,\n last_accessed = excluded.last_accessed\n ");this.indexDb.transaction(()=>{for(const s of e)n.run(s,t,t)})(),T.debug(`Recorded access for ${e.length} chunks`)}getUtilityScores(e){const t=new Map;if(!this.searchDb||0===e.length)return t;if(!this.searchDb.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chunk_utility'").get())return t;const n=e.map(()=>"?").join(","),s=this.searchDb.prepare(`SELECT chunk_id, access_count, last_accessed, first_accessed FROM chunk_utility WHERE chunk_id IN (${n})`).all(...e);for(const e of s)t.set(e.chunk_id,{access_count:e.access_count,last_accessed:e.last_accessed,first_accessed:e.first_accessed});return t}readFile(t,s,i){const r=o(this.memoryDir,t);if(!e(r))return{path:t,content:`[File not found: ${t}]`};const h=n(r,"utf-8"),a=h.split("\n");if(void 0!==s||void 0!==i){const e=Math.max(0,(s??1)-1),n=i??a.length;return{path:t,content:a.slice(e,e+n).join("\n")}}return{path:t,content:h}}listFiles(e,t){const n=D(this.memoryDir),s=[];for(const i of n){const n=d(i.relPath).match(/^(\d{4}-\d{2}-\d{2})/);if(!n)continue;const r=n[1];r>=e&&r<=t&&s.push({path:i.relPath,sessionKey:i.sessionKey,size:i.size})}return s.sort((e,t)=>e.path.localeCompare(t.path)),s}expandChunks(e){if(!this.searchDb||0===e.length)return[];const t=e.map(()=>"?").join(","),n=this.searchDb.prepare(`SELECT id, content, doc_path, role, timestamp FROM chunks WHERE id IN (${t})`).all(...e);return n.length>0&&this.recordAccess(n.map(e=>e.id)),n.map(e=>({chunkId:e.id,content:e.content,path:e.doc_path,role:e.role,timestamp:e.timestamp}))}bm25Search(e,t){if(!this.searchDb)return[];try{const n=function(e){const t=e.replace(/[^\w\s]/g," ").split(/\s+/).filter(e=>e.length>0);return 0===t.length?"":t.map(e=>`"${e}"`).join(" OR ")}(e);if(!n)return[];return this.searchDb.prepare("\n SELECT chunks.id, bm25(chunks_fts) as rank\n FROM chunks_fts\n JOIN chunks ON chunks.id = chunks_fts.rowid\n WHERE chunks_fts MATCH ?\n ORDER BY rank\n LIMIT ?\n ").all(n,t).map(e=>({id:e.id,score:-e.rank}))}catch(e){return T.warn(`BM25 search error: ${e}`),[]}}denseSearch(e,t){if(!this.searchHnsw||0===this.searchHnsw.getCurrentCount())return[];const n=Math.min(t,this.searchHnsw.getCurrentCount()),s=this.searchHnsw.searchKnn(Array.from(e),n);return s.neighbors.map((e,t)=>({id:e,score:1-s.distances[t]}))}startWatcher(){if(e(this.memoryDir))try{this.watcher=a(this.memoryDir,{recursive:!0},(e,t)=>{this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>{this.runIndexCycle()},this.opts.updateDebounceMs)})}catch(e){T.warn(`Could not start file watcher: ${e}`)}}async runIndexCycle(){if(!this.indexing){this.indexing=!0;try{await this.indexFiles(),this.indexDb&&await this.l0Generator.generate(this.indexDb),this.publishSnapshot()}catch(e){T.error(`Index cycle error: ${e}`)}finally{this.indexing=!1}}}async indexFiles(){if(!this.indexDb||!e(this.memoryDir))return;const t=D(this.memoryDir);let s=0;const i=this.indexDb.prepare("INSERT OR REPLACE INTO documents (path, mtime_ms, size) VALUES (?, ?, ?)"),r=this.indexDb.prepare("SELECT mtime_ms, size FROM documents WHERE path = ?"),h=this.indexDb.prepare("DELETE FROM chunks WHERE doc_path = ?"),a=this.indexDb.prepare("SELECT id FROM chunks WHERE doc_path = ?"),o=this.indexDb.prepare("DELETE FROM embeddings WHERE chunk_id IN (SELECT id FROM chunks WHERE doc_path = ?)"),c=this.indexDb.prepare("INSERT INTO chunks (doc_path, chunk_idx, role, timestamp, session_key, content) VALUES (?, ?, ?, ?, ?, ?)"),d=this.indexDb.prepare("SELECT path FROM documents").all().map(e=>e.path),l=new Set(t.map(e=>e.relPath));for(const e of d)if(!l.has(e)){const t=a.all(e);for(const{id:e}of t)try{this.indexHnsw?.markDelete(e)}catch{}o.run(e),h.run(e),this.indexDb.prepare("DELETE FROM documents WHERE path = ?").run(e),T.debug(`Removed deleted file from index: ${e}`)}for(const e of t){const t=r.get(e.relPath);if(t&&t.mtime_ms===e.mtimeMs&&t.size===e.size){s+=this.indexDb.prepare("SELECT COUNT(*) as c FROM chunks WHERE doc_path = ?").get(e.relPath).c;continue}const d=w(n(e.fullPath,"utf-8"),e.relPath,e.sessionKey),l=a.all(e.relPath);for(const{id:e}of l)try{this.indexHnsw?.markDelete(e)}catch{}o.run(e.relPath),h.run(e.relPath);this.indexDb.transaction(()=>{for(let t=0;t<d.length;t++){const n=d[t];c.run(e.relPath,t,n.role,n.timestamp,n.sessionKey,n.content)}i.run(e.relPath,e.mtimeMs,e.size)})(),s+=d.length,T.debug(`Indexed ${e.relPath}: ${d.length} chunks`)}T.info(`Indexed ${s} chunks from ${t.length} files`)}async embedPending(){if(!this.stopped&&!this.embedding&&this.indexDb&&this.indexHnsw){this.embedding=!0;try{const e=this.indexDb.prepare("\n SELECT c.id, c.content FROM chunks c\n LEFT JOIN embeddings e ON e.chunk_id = c.id\n WHERE e.chunk_id IS NULL\n ").all();if(0===e.length)return;T.info(`Embedding ${e.length} pending chunks...`),this.ensureHnswCapacity(e.length);const t=this.indexDb.prepare("INSERT OR REPLACE INTO embeddings (chunk_id) VALUES (?)");for(let n=0;n<e.length;n+=100){if(this.stopped)return void T.warn("embedPending aborted: engine stopped");const s=e.slice(n,n+100),i=s.map(e=>this.applyPrefix(this.opts.prefixDocument,e.content).slice(0,8e3)),r=this.opts.prefixDocument.trim();r&&T.debug(`Using prefixDocument (template: ${r}) → result sample: [${i[0].slice(0,80)}]`);try{let e;if(this.openai){const t=await this.openai.embeddings.create({model:this.opts.embeddingModel,input:i,dimensions:this.opts.embeddingDimensions});e=t.data.sort((e,t)=>e.index-t.index).map(e=>e.embedding)}else e=await this.fetchEmbeddings(i);if(this.stopped||!this.indexDb||!this.indexHnsw)return void T.warn("embedPending aborted: engine stopped during embedding");this.indexDb.transaction(()=>{for(let n=0;n<e.length;n++)this.indexHnsw.addPoint(e[n],s[n].id,!0),t.run(s[n].id)})(),T.debug(`Embedded batch ${n/100+1}: ${s.length} chunks`)}catch(e){if(this.stopped)return;T.error(`Embedding batch failed: ${e}`)}}if(this.stopped||!this.indexHnsw)return;this.indexHnsw.writeIndexSync(this.indexHnswPath),this.publishSnapshot(),T.info(`Embedded ${e.length} chunks (HNSW: ${this.indexHnsw.getCurrentCount()} total points)`)}finally{this.embedding=!1}}}publishSnapshot(){if(!this.indexDb)return;const t=o(this.dataDir,".memory-search-next.tmp"),n=o(this.dataDir,".memory-vectors-search-next.tmp");try{this.indexDb.pragma("wal_checkpoint(TRUNCATE)"),i(this.indexDbPath,t),r(t,this.searchNextDbPath),e(this.indexHnswPath)&&(i(this.indexHnswPath,n),r(n,this.searchNextHnswPath)),T.debug("Published search snapshot (DB + HNSW)")}catch(e){T.error(`Failed to publish snapshot: ${e}`);try{h(t)}catch{}try{h(n)}catch{}}}maybeSwap(){if(e(this.searchNextDbPath))try{this.searchDb&&(this.searchDb.close(),this.searchDb=null),this.searchHnsw=null,r(this.searchNextDbPath,this.searchDbPath),e(this.searchNextHnswPath)&&r(this.searchNextHnswPath,this.searchHnswPath),this.searchDb=new m(this.searchDbPath,{readonly:!0}),e(this.searchHnswPath)?(this.searchHnsw=new E(x,this.opts.embeddingDimensions),this.searchHnsw.readIndexSync(this.searchHnswPath),this.searchHnsw.setEf(50),T.debug(`Swapped to new search DB + HNSW (${this.searchHnsw.getCurrentCount()} points)`)):T.debug("Swapped to new search DB (no HNSW index yet)")}catch(t){T.error(`Failed to swap search DB: ${t}`);try{e(this.searchDbPath)&&(this.searchDb=new m(this.searchDbPath,{readonly:!0}))}catch{}}}applyPrefix(e,t){const n=e.trim();return n?n.replace(/\{content\}/g,()=>t):t}async fetchEmbeddings(e){const t=`${(this.opts.baseURL||"").replace(/\/+$/,"")}/embed`,n={"Content-Type":"application/json"};this.opts.apiKey&&(n.Authorization=`Bearer ${this.opts.apiKey}`);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({model:this.opts.embeddingModel,input:e})});if(!s.ok){const e=await s.text().catch(()=>"(no body)");throw new Error(`Embedding API ${s.status}: ${e.slice(0,300)}`)}const i=await s.json();if(Array.isArray(i.embeddings))return i.embeddings;throw new Error(`Unknown embedding response format. Keys: ${Object.keys(i).join(", ")}`)}async embedText(e){try{const t=this.applyPrefix(this.opts.prefixQuery,e),n=this.opts.prefixQuery.trim();if(n&&T.debug(`Using prefixQuery (template: ${n}) → result sample: [${t.slice(0,80)}]`),this.openai){const e=await this.openai.embeddings.create({model:this.opts.embeddingModel,input:t.slice(0,8e3),dimensions:this.opts.embeddingDimensions});return new Float32Array(e.data[0].embedding)}const s=await this.fetchEmbeddings([t.slice(0,8e3)]);return new Float32Array(s[0])}catch(e){return T.error(`Failed to embed query: ${e}`),null}}}function w(e,t,n){const s=[],i=e.split(/^### /m);for(const e of i){if(!e.trim())continue;const t=e.match(/^(user|assistant)\s*\(([^)]+)\)\s*\n/),i=t?t[1]:"",r=t?t[2]:"",h=t?e.slice(t[0].length).trim():e.trim();if(!h)continue;const a=1500,o=100;if(h.length<=a)s.push({role:i,timestamp:r,sessionKey:n,content:h});else{let e=0;for(;e<h.length;){const t=Math.min(e+a,h.length),c=h.slice(e,t);if(s.push({role:i,timestamp:r,sessionKey:n,content:c}),e=t-o,e+o>=h.length)break}}}return s}function D(e){const n=[];return function i(r){let h;try{h=t(r,{withFileTypes:!0})}catch{return}for(const t of h){const h=o(r,t.name);if(t.isDirectory())i(h);else if(t.name.endsWith(".md"))try{const t=s(h),i=c(e,h),r=d(l(h));n.push({fullPath:h,relPath:i,sessionKey:r===d(e)?"":r,mtimeMs:Math.floor(t.mtimeMs),size:t.size})}catch{}}}(e),n}function y(e,t){if(0===e.size&&0===t.size)return 0;let n=0;const s=e.size<=t.size?e:t,i=e.size<=t.size?t:e;for(const e of s)i.has(e)&&n++;const r=e.size+t.size-n;return 0===r?0:n/r}
1
+ import{existsSync as e,readdirSync as t,readFileSync as n,statSync as s,copyFileSync as i,renameSync as c,unlinkSync as r,watch as a}from"node:fs";import{join as h,relative as o,basename as d,dirname as l}from"node:path";import u from"better-sqlite3";import p from"openai";import m from"hnswlib-node";const{HierarchicalNSW:E}=m;import{createLogger as b}from"../utils/logger.js";import{L0Generator as f,L0_DEFAULT as g}from"./l0-generator.js";const T=b("MemorySearch"),x="cosine";export class MemorySearch{memoryDir;dataDir;opts;indexDb=null;indexHnsw=null;watcher=null;debounceTimer=null;embedTimer=null;indexing=!1;embedding=!1;stopped=!0;l0Generator;searchDb=null;searchHnsw=null;conceptStore=null;conceptCache=null;conceptCacheMap=null;conceptCacheTime=0;openai=null;indexDbPath;searchDbPath;searchNextDbPath;indexHnswPath;searchHnswPath;searchNextHnswPath;constructor(e,t,n){this.memoryDir=e,this.dataDir=t,this.opts=n,this.indexDbPath=h(t,"memory-index.db"),this.searchDbPath=h(t,"memory-search.db"),this.searchNextDbPath=h(t,"memory-search-next.db"),this.indexHnswPath=h(t,"memory-vectors.hnsw"),this.searchHnswPath=h(t,"memory-vectors-search.hnsw"),this.searchNextHnswPath=h(t,"memory-vectors-search-next.hnsw"),this.isOpenAI()&&(this.openai=new p({apiKey:n.apiKey,...n.baseURL?{baseURL:n.baseURL}:{}})),this.l0Generator=new f(n.l0??g)}setConceptStore(e){this.conceptStore=e,T.info("ConceptStore connected — spreading activation enabled"),this.populateChunkEntities()}getConceptCacheEntries(){if(this.conceptCache&&Date.now()-this.conceptCacheTime<36e5)return this.conceptCache;if(!this.conceptStore)return[];const e=this.conceptStore.getConceptCacheData();this.conceptCache=e.filter(e=>e.label.length>=3).map(e=>({id:e.id,label:e.label,labelLower:e.label.toLowerCase(),fan:e.fan,sMax:e.sMax??1.6})),this.conceptCacheMap=new Map;for(const e of this.conceptCache)this.conceptCacheMap.set(e.id,e);return this.conceptCacheTime=Date.now(),T.info(`Concept cache refreshed: ${this.conceptCache.length} entries`),this.conceptCache}extractEntities(e){const t=this.getConceptCacheEntries();if(0===t.length)return[];const n=e.toLowerCase(),s=[];for(const e of t)e.fan>30||n.includes(e.labelLower)&&s.push(e.id);return s}computeSpreadingActivation(e,t,n){if(0===e.length||0===t.length)return 0;const s=this.conceptCacheMap;if(!s)return 0;const i=1/e.length,c=new Set(t);let r=0;for(const t of e){const e=s.get(t);if(!e)continue;const a=Math.max(e.sMax-Math.log(e.fan+1),0);c.has(t)&&(r+=i*a);const h=n.get(t);if(h)for(const{neighbor:e}of h)if(e!==t&&c.has(e)){const t=s.get(e);if(!t)continue;r+=i*Math.max(t.sMax-Math.log(t.fan+1),0)*.5}}return r}populateChunkEntities(){if(!this.indexDb||!this.conceptStore)return;const e=this.indexDb.prepare("SELECT COUNT(*) as c FROM chunk_entities").get().c;if(e>0)return void T.debug(`chunk_entities already populated (${e} rows)`);if(0===this.getConceptCacheEntries().length)return;const t=this.indexDb.prepare("SELECT id, content FROM chunks").all();if(0===t.length)return;const n=this.indexDb.prepare("INSERT OR IGNORE INTO chunk_entities (chunk_id, concept_id) VALUES (?, ?)");let s=0;this.indexDb.transaction(()=>{for(const e of t){const t=this.extractEntities(e.content);for(const i of t)n.run(e.id,i),s++}})(),this.publishSnapshot(),T.info(`Phase 3: Populated chunk_entities — ${s} mappings for ${t.length} chunks`)}getChunkEntities(e,t){if(this.searchDb){const t=this.searchDb.prepare("SELECT concept_id FROM chunk_entities WHERE chunk_id = ?").all(e);if(t.length>0)return t.map(e=>e.concept_id)}return this.extractEntities(t)}isOpenAI(){return(this.opts.baseURL||"https://api.openai.com/v1").includes("openai.com")}getMaxInjectedChars(){return this.opts.maxInjectedChars}async start(){T.info("Starting memory search engine..."),this.stopped=!1,this.indexDb=new u(this.indexDbPath),this.indexDb.pragma("journal_mode = WAL"),this.migrateEmbeddingsTable(),this.indexDb.exec("\n CREATE TABLE IF NOT EXISTS documents (\n path TEXT PRIMARY KEY,\n mtime_ms INTEGER NOT NULL,\n size INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS chunks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n doc_path TEXT NOT NULL,\n chunk_idx INTEGER NOT NULL,\n role TEXT NOT NULL DEFAULT '',\n timestamp TEXT NOT NULL DEFAULT '',\n session_key TEXT NOT NULL DEFAULT '',\n content TEXT NOT NULL,\n UNIQUE(doc_path, chunk_idx)\n );\n\n CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(\n content,\n content='chunks',\n content_rowid='id',\n tokenize='porter unicode61'\n );\n\n CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN\n INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\n CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN\n INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', old.id, old.content);\n END;\n\n CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN\n INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', old.id, old.content);\n INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\n -- Lookup table only (no vector BLOB) — vectors live in HNSW index\n CREATE TABLE IF NOT EXISTS embeddings (\n chunk_id INTEGER PRIMARY KEY\n );\n\n -- Metadata for detecting config changes (model, dimensions)\n CREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n\n -- Phase 3: Pre-computed entity→chunk mapping for spreading activation\n CREATE TABLE IF NOT EXISTS chunk_entities (\n chunk_id INTEGER NOT NULL,\n concept_id TEXT NOT NULL,\n PRIMARY KEY (chunk_id, concept_id)\n );\n CREATE INDEX IF NOT EXISTS idx_chunk_entities_concept ON chunk_entities(concept_id);\n\n -- Utility scoring: tracks how often each chunk is retrieved and used\n CREATE TABLE IF NOT EXISTS chunk_utility (\n chunk_id INTEGER PRIMARY KEY,\n access_count INTEGER DEFAULT 0,\n last_accessed TEXT,\n first_accessed TEXT,\n importance INTEGER DEFAULT 5,\n access_times TEXT DEFAULT '[]'\n );\n"),this.migrateChunkUtility(),this.checkEmbeddingConfigChange(),this.initIndexHnsw(),await this.indexFiles(),await this.embedPending(),this.indexDb&&await this.l0Generator.start(this.indexDb),this.publishSnapshot(),this.maybeSwap(),this.startWatcher(),this.opts.embedIntervalMs>0&&(this.embedTimer=setInterval(()=>{this.embedPending().catch(e=>T.error(`Embed cycle error: ${e}`))},this.opts.embedIntervalMs)),this.conceptStore&&this.populateChunkEntities(),T.info("Memory search engine started")}stop(){this.stopped=!0,this.watcher&&(this.watcher.close(),this.watcher=null),this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.embedTimer&&(clearInterval(this.embedTimer),this.embedTimer=null),this.indexDb&&(this.indexDb.close(),this.indexDb=null),this.searchDb&&(this.searchDb.close(),this.searchDb=null),this.indexHnsw=null,this.searchHnsw=null,this.l0Generator.stop(),T.info("Memory search engine stopped")}migrateEmbeddingsTable(){if(!this.indexDb)return;if(this.indexDb.prepare("PRAGMA table_info(embeddings)").all().some(e=>"vector"===e.name)){T.info("Migrating: dropping old embeddings table (had vector BLOB). All embeddings will be re-created via HNSW."),this.indexDb.exec("DROP TABLE IF EXISTS embeddings");try{r(this.indexHnswPath)}catch{}}}migrateChunkUtility(){for(const e of[this.indexDb,this.searchDb]){if(!e)continue;if(!e.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chunk_utility'").get())continue;const t=e.prepare("PRAGMA table_info(chunk_utility)").all(),n=new Set(t.map(e=>e.name));n.has("importance")||(T.info("Migrating chunk_utility: adding importance column"),e.exec("ALTER TABLE chunk_utility ADD COLUMN importance INTEGER DEFAULT 5")),n.has("access_times")||(T.info("Migrating chunk_utility: adding access_times column"),e.exec("ALTER TABLE chunk_utility ADD COLUMN access_times TEXT DEFAULT '[]'"))}}checkEmbeddingConfigChange(){if(!this.indexDb)return;const e=this.indexDb.prepare("SELECT value FROM meta WHERE key = ?"),t=this.indexDb.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)"),n=e.get("embedding_model")?.value,s=e.get("embedding_dimensions")?.value,i=this.opts.embeddingModel,c=String(this.opts.embeddingDimensions),a=void 0!==n&&n!==i,h=void 0!==s&&s!==c;if(a||h){const e=[];a&&e.push(`model: ${n} → ${i}`),h&&e.push(`dimensions: ${s} → ${c}`),T.info(`Embedding config changed (${e.join(", ")}). Wiping embeddings + HNSW for full re-embed.`),this.indexDb.exec("DELETE FROM embeddings");try{r(this.indexHnswPath)}catch{}}t.run("embedding_model",i),t.run("embedding_dimensions",c)}initIndexHnsw(){const t=this.opts.embeddingDimensions;if(this.indexHnsw=new E(x,t),e(this.indexHnswPath))try{this.indexHnsw.readIndexSync(this.indexHnswPath,!0),T.info(`Loaded HNSW index: ${this.indexHnsw.getCurrentCount()} points`)}catch(e){T.warn(`Failed to load HNSW index, creating new: ${e}`),this.indexHnsw.initIndex({maxElements:1e4,m:16,efConstruction:200,allowReplaceDeleted:!0})}else this.indexHnsw.initIndex({maxElements:1e4,m:16,efConstruction:200,allowReplaceDeleted:!0}),T.info("Created new HNSW index")}ensureHnswCapacity(e){if(!this.indexHnsw)return;const t=this.indexHnsw.getMaxElements();if(this.indexHnsw.getCurrentCount()+e>t){const n=Math.max(2*t,this.indexHnsw.getCurrentCount()+e+1e3);this.indexHnsw.resizeIndex(n),T.info(`Resized HNSW index: ${t} → ${n}`)}}async search(e,t){const n=t??this.opts.maxResults;if(this.maybeSwap(),!this.searchDb)return T.warn("Search DB not available"),[];const s=this.bm25Search(e,20);let i=[];try{const t=await this.embedText(e);t&&this.searchHnsw&&this.searchHnsw.getCurrentCount()>0&&(i=this.denseSearch(t,20))}catch(e){T.warn(`Dense search failed, using BM25 only: ${e}`)}const c=function(e,t,n){const s=new Map;for(let t=0;t<e.length;t++){const{id:i}=e[t];s.set(i,(s.get(i)??0)+1/(n+t+1))}for(let e=0;e<t.length;e++){const{id:i}=t[e];s.set(i,(s.get(i)??0)+1/(n+e+1))}const i=Array.from(s.entries()).map(([e,t])=>({id:e,score:t})).sort((e,t)=>t.score-e.score);return i}(s,i,this.opts.rrfK),r=Math.min(c.length,40),a=this.searchDb.prepare("SELECT doc_path, role, timestamp, session_key, content, l0 FROM chunks WHERE id = ?"),h=[];for(let e=0;e<r;e++){const{id:t,score:n}=c[e],s=a.get(t);s&&h.push({id:t,score:n,row:s})}const o=36e5,d=this.getUtilityScores(h.map(e=>e.id)),l=Date.now();for(const e of h){const t=d.get(e.id),n=t?.access_times??[],s=t?.importance??5;let i=0;if(n.length>0)for(const e of n){const t=Math.max((l-e)/o,.1);i+=Math.pow(t,-.5)}else if(e.row.timestamp){const t=new Date(e.row.timestamp).getTime();if(!isNaN(t)){const e=Math.max((l-t)/o,.1);i=Math.pow(e,-.5)}}if(i>0){const t=Math.log(i),n=.02*(s-5);e.score+=.01*t+n}}if(this.conceptStore){const t=this.extractEntities(e);if(t.length>0){const e=this.conceptStore.getNeighbors(t);let n=0;for(const s of h){const i=this.getChunkEntities(s.id,s.row.content),c=this.computeSpreadingActivation(t,i,e);c>0&&(s.score+=.015*c,n++)}n>0&&T.debug(`Spreading activation: boosted ${n}/${h.length} candidates (query entities: [${t.join(", ")}])`)}}h.sort((e,t)=>t.score-e.score);const u=function(e,t,n){if(e.length<=t)return e;const s=e.map(e=>function(e){const t=new Set;for(const n of e.toLowerCase().matchAll(/\b\w{2,}\b/g))t.add(n[0]);return t}(e.row.content)),i=e[0]?.score??1,c=e[e.length-1]?.score??0,r=i-c||1,a=[],h=new Set(e.map((e,t)=>t));a.push(0),h.delete(0);for(;a.length<t&&h.size>0;){let t=-1,i=-1/0;for(const o of h){const h=(e[o].score-c)/r;let d=0;for(const e of a){const t=S(s[o],s[e]);t>d&&(d=t)}const l=n*h-(1-n)*d;l>i&&(i=l,t=o)}if(t<0)break;a.push(t),h.delete(t)}return a.map(t=>e[t])}(h,n,.7),p=[];for(const{id:e,score:t,row:n}of u){const s=n.l0&&this.l0Generator.enabled?n.l0:n.content.length>this.opts.maxSnippetChars?n.content.slice(0,this.opts.maxSnippetChars)+"...":n.content;p.push({chunkId:e,path:n.doc_path,sessionKey:n.session_key,snippet:s,score:t,role:n.role,timestamp:n.timestamp})}return T.info(`Search "${e.slice(0,60)}": ${p.length} results (sparse=${s.length}, dense=${i.length}, candidates=${h.length}, mmr=${u.length})`),p}recordAccess(e){if(!this.searchDb||0===e.length)return;const t=(new Date).toISOString(),n=Date.now(),s=this.searchDb.prepare("\n INSERT INTO chunk_utility (chunk_id, access_count, last_accessed, first_accessed, access_times)\n VALUES (?, 1, ?, ?, ?)\n ON CONFLICT(chunk_id) DO UPDATE SET\n access_count = access_count + 1,\n last_accessed = excluded.last_accessed,\n access_times = excluded.access_times\n "),i=this.searchDb.prepare("SELECT access_times FROM chunk_utility WHERE chunk_id = ?");this.searchDb.transaction(()=>{for(const c of e){const e=i.get(c);let r=[];if(e?.access_times)try{r=JSON.parse(e.access_times)}catch{}r.push(n),r.length>50&&(r=r.slice(-50)),s.run(c,t,t,JSON.stringify(r))}})(),T.debug(`Recorded access for ${e.length} chunks`)}getRecentlyAccessedChunks(e=7,t=100){if(!this.searchDb)return[];if(!this.searchDb.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chunk_utility'").get())return[];const n=new Date(Date.now()-864e5*e).toISOString();return this.searchDb.prepare("\n SELECT cu.chunk_id, c.content, c.doc_path, cu.importance, cu.access_count, cu.last_accessed\n FROM chunk_utility cu\n JOIN chunks c ON c.id = cu.chunk_id\n WHERE cu.last_accessed >= ? AND cu.access_count > 0\n ORDER BY cu.access_count DESC, cu.last_accessed DESC\n LIMIT ?\n ").all(n,t).map(e=>({chunkId:e.chunk_id,content:e.content.slice(0,500),path:e.doc_path,importance:e.importance??5,accessCount:e.access_count,lastAccessed:e.last_accessed}))}updateImportance(e){if(!this.searchDb||0===e.length)return 0;const t=this.searchDb.prepare("UPDATE chunk_utility SET importance = ? WHERE chunk_id = ?");let n=0;return this.searchDb.transaction(()=>{for(const{chunkId:s,importance:i}of e){const e=Math.max(1,Math.min(10,Math.round(i)));t.run(e,s).changes>0&&n++}})(),T.info(`Updated importance for ${n}/${e.length} chunks`),n}getUtilityScores(e){const t=new Map;if(!this.searchDb||0===e.length)return t;if(!this.searchDb.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chunk_utility'").get())return t;const n=e.map(()=>"?").join(","),s=this.searchDb.prepare(`SELECT chunk_id, access_count, last_accessed, first_accessed, importance, access_times FROM chunk_utility WHERE chunk_id IN (${n})`).all(...e);for(const e of s){let n=[];if(e.access_times)try{n=JSON.parse(e.access_times)}catch{}t.set(e.chunk_id,{access_count:e.access_count,last_accessed:e.last_accessed,first_accessed:e.first_accessed,importance:e.importance??5,access_times:n})}return t}readFile(t,s,i){const c=h(this.memoryDir,t);if(!e(c))return{path:t,content:`[File not found: ${t}]`};const r=n(c,"utf-8"),a=r.split("\n");if(void 0!==s||void 0!==i){const e=Math.max(0,(s??1)-1),n=i??a.length;return{path:t,content:a.slice(e,e+n).join("\n")}}return{path:t,content:r}}listFiles(e,t){const n=_(this.memoryDir),s=[];for(const i of n){const n=d(i.relPath).match(/^(\d{4}-\d{2}-\d{2})/);if(!n)continue;const c=n[1];c>=e&&c<=t&&s.push({path:i.relPath,sessionKey:i.sessionKey,size:i.size})}return s.sort((e,t)=>e.path.localeCompare(t.path)),s}expandChunks(e){if(!this.searchDb||0===e.length)return[];const t=e.map(()=>"?").join(","),n=this.searchDb.prepare(`SELECT id, content, doc_path, role, timestamp FROM chunks WHERE id IN (${t})`).all(...e);return n.length>0&&this.recordAccess(n.map(e=>e.id)),n.map(e=>({chunkId:e.id,content:e.content,path:e.doc_path,role:e.role,timestamp:e.timestamp}))}bm25Search(e,t){if(!this.searchDb)return[];try{const n=function(e){const t=e.replace(/[^\w\s]/g," ").split(/\s+/).filter(e=>e.length>0);return 0===t.length?"":t.map(e=>`"${e}"`).join(" OR ")}(e);if(!n)return[];return this.searchDb.prepare("\n SELECT chunks.id, bm25(chunks_fts) as rank\n FROM chunks_fts\n JOIN chunks ON chunks.id = chunks_fts.rowid\n WHERE chunks_fts MATCH ?\n ORDER BY rank\n LIMIT ?\n ").all(n,t).map(e=>({id:e.id,score:-e.rank}))}catch(e){return T.warn(`BM25 search error: ${e}`),[]}}denseSearch(e,t){if(!this.searchHnsw||0===this.searchHnsw.getCurrentCount())return[];const n=Math.min(t,this.searchHnsw.getCurrentCount()),s=this.searchHnsw.searchKnn(Array.from(e),n);return s.neighbors.map((e,t)=>({id:e,score:1-s.distances[t]}))}startWatcher(){if(e(this.memoryDir))try{this.watcher=a(this.memoryDir,{recursive:!0},(e,t)=>{this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>{this.runIndexCycle()},this.opts.updateDebounceMs)})}catch(e){T.warn(`Could not start file watcher: ${e}`)}}async runIndexCycle(){if(!this.indexing){this.indexing=!0;try{await this.indexFiles(),this.indexDb&&await this.l0Generator.generate(this.indexDb),this.publishSnapshot()}catch(e){T.error(`Index cycle error: ${e}`)}finally{this.indexing=!1}}}async indexFiles(){if(!this.indexDb||!e(this.memoryDir))return;const t=_(this.memoryDir);let s=0;const i=this.indexDb.prepare("INSERT OR REPLACE INTO documents (path, mtime_ms, size) VALUES (?, ?, ?)"),c=this.indexDb.prepare("SELECT mtime_ms, size FROM documents WHERE path = ?"),r=this.indexDb.prepare("DELETE FROM chunks WHERE doc_path = ?"),a=this.indexDb.prepare("SELECT id FROM chunks WHERE doc_path = ?"),h=this.indexDb.prepare("DELETE FROM embeddings WHERE chunk_id IN (SELECT id FROM chunks WHERE doc_path = ?)"),o=this.indexDb.prepare("INSERT INTO chunks (doc_path, chunk_idx, role, timestamp, session_key, content) VALUES (?, ?, ?, ?, ?, ?)"),d=this.indexDb.prepare("\n INSERT INTO chunk_utility (chunk_id, importance)\n VALUES (?, ?)\n ON CONFLICT(chunk_id) DO UPDATE SET\n importance = excluded.importance\n "),l=this.indexDb.prepare("INSERT OR IGNORE INTO chunk_entities (chunk_id, concept_id) VALUES (?, ?)"),u=this.indexDb.prepare("DELETE FROM chunk_entities WHERE chunk_id IN (SELECT id FROM chunks WHERE doc_path = ?)"),p=this.indexDb.prepare("SELECT path FROM documents").all().map(e=>e.path),m=new Set(t.map(e=>e.relPath));for(const e of p)if(!m.has(e)){const t=a.all(e);for(const{id:e}of t)try{this.indexHnsw?.markDelete(e)}catch{}h.run(e),r.run(e),this.indexDb.prepare("DELETE FROM documents WHERE path = ?").run(e),T.debug(`Removed deleted file from index: ${e}`)}for(const e of t){const t=c.get(e.relPath);if(t&&t.mtime_ms===e.mtimeMs&&t.size===e.size){s+=this.indexDb.prepare("SELECT COUNT(*) as c FROM chunks WHERE doc_path = ?").get(e.relPath).c;continue}const p=w(n(e.fullPath,"utf-8"),e.relPath,e.sessionKey),m=a.all(e.relPath);for(const{id:e}of m)try{this.indexHnsw?.markDelete(e)}catch{}u.run(e.relPath),h.run(e.relPath),r.run(e.relPath);this.indexDb.transaction(()=>{for(let t=0;t<p.length;t++){const n=p[t],s=o.run(e.relPath,t,n.role,n.timestamp,n.sessionKey,n.content),i=Number(s.lastInsertRowid),c=D(n.content,e.relPath);if(5!==c&&d.run(i,c),this.conceptStore){const e=this.extractEntities(n.content);for(const t of e)l.run(i,t)}}i.run(e.relPath,e.mtimeMs,e.size)})(),s+=p.length,T.debug(`Indexed ${e.relPath}: ${p.length} chunks`)}T.info(`Indexed ${s} chunks from ${t.length} files`)}async embedPending(){if(!this.stopped&&!this.embedding&&this.indexDb&&this.indexHnsw){this.embedding=!0;try{const e=this.indexDb.prepare("\n SELECT c.id, c.content FROM chunks c\n LEFT JOIN embeddings e ON e.chunk_id = c.id\n WHERE e.chunk_id IS NULL\n ").all();if(0===e.length)return;T.info(`Embedding ${e.length} pending chunks...`),this.ensureHnswCapacity(e.length);const t=this.indexDb.prepare("INSERT OR REPLACE INTO embeddings (chunk_id) VALUES (?)");for(let n=0;n<e.length;n+=100){if(this.stopped)return void T.warn("embedPending aborted: engine stopped");const s=e.slice(n,n+100),i=s.map(e=>this.applyPrefix(this.opts.prefixDocument,e.content).slice(0,8e3)),c=this.opts.prefixDocument.trim();c&&T.debug(`Using prefixDocument (template: ${c}) → result sample: [${i[0].slice(0,80)}]`);try{let e;if(this.openai){const t=await this.openai.embeddings.create({model:this.opts.embeddingModel,input:i,dimensions:this.opts.embeddingDimensions});e=t.data.sort((e,t)=>e.index-t.index).map(e=>e.embedding)}else e=await this.fetchEmbeddings(i);if(this.stopped||!this.indexDb||!this.indexHnsw)return void T.warn("embedPending aborted: engine stopped during embedding");this.indexDb.transaction(()=>{for(let n=0;n<e.length;n++)this.indexHnsw.addPoint(e[n],s[n].id,!0),t.run(s[n].id)})(),T.debug(`Embedded batch ${n/100+1}: ${s.length} chunks`)}catch(e){if(this.stopped)return;T.error(`Embedding batch failed: ${e}`)}}if(this.stopped||!this.indexHnsw)return;this.indexHnsw.writeIndexSync(this.indexHnswPath),this.publishSnapshot(),T.info(`Embedded ${e.length} chunks (HNSW: ${this.indexHnsw.getCurrentCount()} total points)`)}finally{this.embedding=!1}}}publishSnapshot(){if(!this.indexDb)return;const t=h(this.dataDir,".memory-search-next.tmp"),n=h(this.dataDir,".memory-vectors-search-next.tmp");try{this.indexDb.pragma("wal_checkpoint(TRUNCATE)"),i(this.indexDbPath,t),c(t,this.searchNextDbPath),e(this.indexHnswPath)&&(i(this.indexHnswPath,n),c(n,this.searchNextHnswPath)),T.debug("Published search snapshot (DB + HNSW)")}catch(e){T.error(`Failed to publish snapshot: ${e}`);try{r(t)}catch{}try{r(n)}catch{}}}maybeSwap(){if(e(this.searchNextDbPath))try{this.searchDb&&(this.searchDb.close(),this.searchDb=null),this.searchHnsw=null,c(this.searchNextDbPath,this.searchDbPath),e(this.searchNextHnswPath)&&c(this.searchNextHnswPath,this.searchHnswPath),this.searchDb=new u(this.searchDbPath),this.searchDb.pragma("journal_mode = WAL"),this.migrateChunkUtility(),e(this.searchHnswPath)?(this.searchHnsw=new E(x,this.opts.embeddingDimensions),this.searchHnsw.readIndexSync(this.searchHnswPath),this.searchHnsw.setEf(50),T.debug(`Swapped to new search DB + HNSW (${this.searchHnsw.getCurrentCount()} points)`)):T.debug("Swapped to new search DB (no HNSW index yet)")}catch(t){T.error(`Failed to swap search DB: ${t}`);try{e(this.searchDbPath)&&(this.searchDb=new u(this.searchDbPath))}catch{}}}applyPrefix(e,t){const n=e.trim();return n?n.replace(/\{content\}/g,()=>t):t}async fetchEmbeddings(e){const t=`${(this.opts.baseURL||"").replace(/\/+$/,"")}/embed`,n={"Content-Type":"application/json"};this.opts.apiKey&&(n.Authorization=`Bearer ${this.opts.apiKey}`);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({model:this.opts.embeddingModel,input:e})});if(!s.ok){const e=await s.text().catch(()=>"(no body)");throw new Error(`Embedding API ${s.status}: ${e.slice(0,300)}`)}const i=await s.json();if(Array.isArray(i.embeddings))return i.embeddings;throw new Error(`Unknown embedding response format. Keys: ${Object.keys(i).join(", ")}`)}async embedText(e){try{const t=this.applyPrefix(this.opts.prefixQuery,e),n=this.opts.prefixQuery.trim();if(n&&T.debug(`Using prefixQuery (template: ${n}) → result sample: [${t.slice(0,80)}]`),this.openai){const e=await this.openai.embeddings.create({model:this.opts.embeddingModel,input:t.slice(0,8e3),dimensions:this.opts.embeddingDimensions});return new Float32Array(e.data[0].embedding)}const s=await this.fetchEmbeddings([t.slice(0,8e3)]);return new Float32Array(s[0])}catch(e){return T.error(`Failed to embed query: ${e}`),null}}}function D(e,t){let n=5;return"MEMORY.md"===t&&(n=9),t.startsWith("knowledge/")&&(n=8),/\b(decid|commit|important|critical|must|priority|never|always)\b/i.test(e)&&(n=Math.max(n,7)),/\b(password|key|token|secret|wallet|address)\b/i.test(e)&&(n=Math.max(n,8)),/\b(ok|thanks|got it|sure|yes|no|grazie|va bene)\b/i.test(e)&&e.length<80&&(n=Math.min(n,2)),/\d{4}-\d{2}-\d{2}/.test(e)&&(n=Math.max(n,6)),/https?:\/\//.test(e)&&(n=Math.max(n,6)),/\b(TODO|FIXME|HACK|BUG)\b/.test(e)&&(n=Math.max(n,7)),Math.min(10,Math.max(1,n))}function w(e,t,n){const s=[],i=e.split(/^### /m);for(const e of i){if(!e.trim())continue;const t=e.match(/^(user|assistant)\s*\(([^)]+)\)\s*\n/),i=t?t[1]:"",c=t?t[2]:"",r=t?e.slice(t[0].length).trim():e.trim();if(!r)continue;const a=1500,h=100;if(r.length<=a)s.push({role:i,timestamp:c,sessionKey:n,content:r});else{let e=0;for(;e<r.length;){const t=Math.min(e+a,r.length),o=r.slice(e,t);if(s.push({role:i,timestamp:c,sessionKey:n,content:o}),e=t-h,e+h>=r.length)break}}}return s}function _(e){const n=[];return function i(c){let r;try{r=t(c,{withFileTypes:!0})}catch{return}for(const t of r){const r=h(c,t.name);if(t.isDirectory())i(r);else if(t.name.endsWith(".md"))try{const t=s(r),i=o(e,r),c=d(l(r));n.push({fullPath:r,relPath:i,sessionKey:c===d(e)?"":c,mtimeMs:Math.floor(t.mtimeMs),size:t.size})}catch{}}}(e),n}function S(e,t){if(0===e.size&&0===t.size)return 0;let n=0;const s=e.size<=t.size?e:t,i=e.size<=t.size?t:e;for(const e of s)i.has(e)&&n++;const c=e.size+t.size-n;return 0===c?0:n/c}
package/dist/server.d.ts CHANGED
@@ -16,6 +16,7 @@ export declare class Server {
16
16
  private sessionDb;
17
17
  private nodeSignatureDb;
18
18
  private channelManager;
19
+ private typingCoordinator;
19
20
  private agentService;
20
21
  private sessionManager;
21
22
  private memoryManager;
@@ -49,6 +50,8 @@ export declare class Server {
49
50
  private triageHeartbeat;
50
51
  private setupCommands;
51
52
  private registerChannels;
53
+ /** Handle an incoming user reaction — annotate in memory file (no API call). */
54
+ private handleReaction;
52
55
  handleMessage(msg: IncomingMessage, isRetry?: boolean): Promise<string>;
53
56
  start(): Promise<void>;
54
57
  private initCronAndHeartbeat;
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as o,resolve as i}from"node:path";import{TokenDB as r}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as h}from"./agent/session-db.js";import{ChannelManager as c}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram/index.js";import{WhatsAppChannel as l}from"./gateway/channels/whatsapp.js";import{WebChatChannel as m}from"./gateway/channels/webchat.js";import{MeshChannel as d}from"./gateway/channels/mesh.js";import{ResponsesChannel as f}from"./channels/responses.js";import{AgentService as p}from"./agent/agent-service.js";import{SessionManager as u}from"./agent/session-manager.js";import{buildPrompt as b,buildSystemPrompt as y}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as S,loadWorkspaceFiles as w}from"./agent/workspace-files.js";import{NodeRegistry as v}from"./gateway/node-registry.js";import{MemoryManager as M}from"./memory/memory-manager.js";import{MessageProcessor as R}from"./media/message-processor.js";import{loadSTTProvider as C}from"./stt/stt-loader.js";import{CommandRegistry as A}from"./commands/command-registry.js";import{NewCommand as T}from"./commands/new.js";import{CompactCommand as $}from"./commands/compact.js";import{ModelCommand as k,DefaultModelCommand as j}from"./commands/model.js";import{StopCommand as I}from"./commands/stop.js";import{HelpCommand as x}from"./commands/help.js";import{McpCommand as D}from"./commands/mcp.js";import{ModelsCommand as E}from"./commands/models.js";import{CoderCommand as _}from"./commands/coder.js";import{SandboxCommand as P}from"./commands/sandbox.js";import{SubAgentsCommand as N}from"./commands/subagents.js";import{CustomSubAgentsCommand as U}from"./commands/customsubagents.js";import{StatusCommand as F}from"./commands/status.js";import{ShowToolCommand as K}from"./commands/showtool.js";import{UsageCommand as O}from"./commands/usage.js";import{DebugA2UICommand as H}from"./commands/debuga2ui.js";import{DebugDynamicCommand as B}from"./commands/debugdynamic.js";import{CronService as L}from"./cron/cron-service.js";import{stripHeartbeatToken as Q,isHeartbeatContentEffectivelyEmpty as W}from"./cron/heartbeat-token.js";import{createServerToolsServer as z}from"./tools/server-tools.js";import{createCronToolsServer as G}from"./tools/cron-tools.js";import{createTTSToolsServer as q}from"./tools/tts-tools.js";import{createMemoryToolsServer as V}from"./tools/memory-tools.js";import{createBrowserToolsServer as J}from"./tools/browser-tools.js";import{createPicoToolsServer as X}from"./tools/pico-tools.js";import{createPlasmaClientToolsServer as Y}from"./tools/plasma-client-tools.js";import{createConceptToolsServer as Z}from"./tools/concept-tools.js";import{BrowserService as ee}from"./browser/browser-service.js";import{MemorySearch as te}from"./memory/memory-search.js";import{ConceptStore as se}from"./memory/concept-store.js";import{stripMediaLines as ne}from"./utils/media-response.js";import{loadConfig as oe,loadRawConfig as ie,backupConfig as re,resolveModelEntry as ae,modelRefName as he}from"./config.js";import{stringify as ce}from"yaml";import{createLogger as ge}from"./utils/logger.js";import{SessionErrorHandler as le}from"./agent/session-error-handler.js";import{initStickerCache as me}from"./gateway/channels/telegram/stickers.js";const de=ge("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsFactory;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;conceptStore=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;meshChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new r(e.dbPath),this.sessionDb=new h(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new v,this.sessionManager=new u(this.sessionDb),e.memory.enabled&&(this.memoryManager=new M(e.memoryDir,e.timezone));const t=C(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new R(t,n),me(e.dataDir),this.commandRegistry=new A,this.setupCommands(),this.channelManager=new c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsFactory=()=>z(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.createMemorySearch(e),this.browserService=new ee,this.conceptStore=new se(e.dataDir);const i=o(e.dataDir,"CONCEPTS.md");this.conceptStore.importFromTurtleIfEmpty(i);const g=o(e.agent.workspacePath,".plasma"),l=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new p(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>G(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>q(()=>this.config):void 0,this.memorySearch?()=>V(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>J({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,l?()=>X({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=w(this.config.dataDir);return y({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>Y({plasmaRootDir:g,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Z(this.conceptStore):void 0),S(e.dataDir),s(o(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(o(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(o(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new L({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e),sessionReaper:{pruneStaleSessions:e=>this.sessionDb.pruneStaleCronSessions(e)}})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=ae(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new te(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK,l0:e.memory.l0??{enabled:!0,model:""}}),V(this.memorySearch);de.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const o=`${s}:${n}`;e.has(o)||(e.add(o),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),o=e.sessionKey.substring(t+1);"cron"!==n&&o&&(this.channelManager.getAdapter(n)&&s(n,o))}return t}async executeCronJob(e){const t=this.config.cron.broadcastEvents;if(!t&&!this.channelManager.getAdapter(e.channel))return de.warn(`Cron job "${e.name}": skipped (channel "${e.channel}" is not active)`),{response:"",delivered:!1};if(e.suppressToken&&"__heartbeat"===e.name){const t=this.triageHeartbeat();if(!t.shouldRun)return de.info(`Cron job "${e.name}": skipped by triage (${t.reason})`),{response:"",delivered:!1};de.info(`Cron job "${e.name}": triage passed (${t.reason})`)}const s="boolean"==typeof e.isolated?e.isolated:this.config.cron.isolated,n=s?"cron":e.channel,o=s?e.name:e.chatId;de.info(`Cron job "${e.name}": session=${n}:${o}, delivery=${e.channel}:${e.chatId}${t?" (broadcast)":""}`);const i={chatId:o,userId:"cron",channelName:n,text:e.message,attachments:[]},r=`${n}:${o}`;try{const s=await this.handleMessage(i);let n=s;if(e.suppressToken){const t=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:o,text:i}=Q(s,t);if(o)return de.info(`Cron job "${e.name}": response suppressed (HEARTBEAT_OK)`),{response:s,delivered:!1};n=i}if(t){const t=this.collectBroadcastTargets();de.info(`Cron job "${e.name}": broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,n))),await Promise.allSettled(t.map(e=>this.channelManager.releaseTyping(e.channel,e.chatId)))}else await this.channelManager.sendResponse(e.channel,e.chatId,n),await this.channelManager.releaseTyping(e.channel,e.chatId).catch(()=>{});return{response:n,delivered:!0}}finally{this.agentService.destroySession(r),this.sessionManager.resetSession(r),this.memoryManager&&this.memoryManager.clearSession(r),de.info(`Cron job "${e.name}": ephemeral session destroyed (${r})`)}}triageHeartbeat(){const t=(new Date).getHours(),s=o(this.config.agent.workspacePath,"attention","pending_signals.md");if(n(s))try{const t=e(s,"utf-8").trim().split("\n").filter(e=>e.trim()&&!e.startsWith("#"));if(t.length>0)return{shouldRun:!0,reason:`${t.length} pending signal(s)`}}catch{}const i=o(this.config.dataDir,"HEARTBEAT.md");let r=!1;if(n(i))try{const t=e(i,"utf-8");r=!W(t)}catch{r=!0}const a=this.sessionDb.hasRecentActivity(3e5);return t>=23||t<7?a?{shouldRun:!0,reason:"night mode but recent messages"}:{shouldRun:!1,reason:"night mode, no activity"}:r||a?{shouldRun:!0,reason:a?"recent messages":"actionable heartbeat"}:{shouldRun:!1,reason:"no signals, no messages, empty heartbeat"}}setupCommands(){this.commandRegistry.register(new T),this.commandRegistry.register(new $);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new k(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),i=this.sessionManager.getModel(e)||this.config.agent.model,r=ae(this.config,i),a=o(r?.name??he(i)),h=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const c=a||h;return c&&this.sessionManager.resetSession(e),c},e)),this.commandRegistry.register(new j(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=i(process.cwd(),"config.yaml"),s=ie(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),re(e),t(e,ce(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new E(()=>this.config.models??[],e)),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new K(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new U(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new F(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=ae(this.config,t),o=s?ae(this.config,s):void 0;return{configDefaultModel:he(this.config.agent.model),agentModel:n?.id??t,agentModelName:n?.name??he(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?he(s):void 0),fallbackActive:this.agentService.isFallbackActive(e),coderSkill:this.getChatSetting(e,"coderSkill")??this.coderSkill,showToolUse:this.getChatSetting(e,"showToolUse")??this.showToolUse,subagentsEnabled:this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,customSubAgentsEnabled:this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,sandboxEnabled:this.getChatSetting(e,"sandboxEnabled")??!1,connectedNodes:this.nodeRegistry.listNodes().map(e=>({nodeId:e.nodeId,displayName:e.displayName,hostname:e.hostname}))}})),this.commandRegistry.register(new I(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new D(()=>this.agentService.getToolServers())),this.commandRegistry.register(new x(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new O(e=>this.agentService.getUsage(e))),this.commandRegistry.register(new H(this.nodeRegistry)),this.commandRegistry.register(new B(this.nodeRegistry))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){de.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,t,this.tokenDb,this.config.agent.inflightTyping);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new l(s,this.config.agent.inflightTyping);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new f({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}if(this.config.channels.mesh.enabled){const e=this.config.channels.mesh;e.agentId&&e.privateKey?(this.meshChannel||(this.meshChannel=new d({brokerUrl:e.brokerUrl,agentId:e.agentId,privateKey:e.privateKey,reconnectDelayMs:e.reconnectDelayMs}),this.meshChannel.onReply((e,t,s)=>{de.info(`[Mesh] Routing reply from ${t} to origin session ${e}`);const n=e.indexOf(":"),o=n>0?e.substring(0,n):"mesh",i=n>0?e.substring(n+1):t,r={chatId:i,userId:t,channelName:o,text:`[Mesh reply from ${t}] ${s}`,attachments:[],username:t};this.handleMessage(r).then(t=>{t&&t.trim()&&this.channelManager.sendResponse(o,i,t).catch(t=>{de.error(`[Mesh] Failed to deliver reply-response to ${e}: ${t}`)})}).catch(s=>{de.error(`[Mesh] Failed to process reply from ${t} in session ${e}: ${s}`)})})),this.channelManager.registerAdapter(this.meshChannel)):de.warn("Mesh channel enabled but agentId or privateKey not configured")}this.webChatChannel||(this.webChatChannel=new m),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e,t=!1){const s=`${e.channelName}:${e.chatId}`,n=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";de.info(`Message from ${s} (user=${e.userId}, ${e.username??"?"}): ${n}`),this.config.verboseDebugLogs&&de.debug(`Message from ${s} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const t=e.text.substring(6);return this.agentService.resolveQuestion(s,t),""}if(this.agentService.hasPendingQuestion(s)){const t=e.text.trim();return this.agentService.resolveQuestion(s,t),`Selected: ${t}`}if(e.text.startsWith("__tool_perm:")){const t="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(s,t),t?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(s)){const t=e.text.trim().toLowerCase();if("approve"===t||"approva"===t)return this.agentService.resolvePermission(s,!0),"Tool approved.";if("deny"===t||"vieta"===t||"blocca"===t)return this.agentService.resolvePermission(s,!1),"Tool denied."}}const n=!0===e.__passthrough;if(!n&&e.text&&this.commandRegistry.isCommand(e.text)){const t=await this.commandRegistry.dispatch(e.text,{sessionKey:s,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(t)return t.passthrough?this.handleMessage({...e,text:t.passthrough,__passthrough:!0}):(t.resetSession?(this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s)):t.resetAgent&&this.agentService.destroySession(s),t.buttons&&t.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,t.text,t.buttons),""):t.text)}if(!n&&e.text?.startsWith("/"))return this.agentService.isBusy(s)?"I'm busy right now. Please resend this request later.":this.handleMessage({...e,text:e.text,__passthrough:!0});const o=this.sessionManager.getOrCreate(s),i=await this.messageProcessor.process(e),r=b(i,void 0,{sessionKey:s,channel:e.channelName,chatId:e.chatId},this.config.timezone);de.debug(`[${s}] Prompt to agent (${r.text.length} chars): ${this.config.verboseDebugLogs?r.text:r.text.slice(0,15)+"..."}${r.images.length>0?` [+${r.images.length} image(s)]`:""}`);const a=o.model,h={sessionKey:s,channel:e.channelName,chatId:e.chatId,sessionId:o.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(s):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(s):""},c=w(this.config.dataDir),g={config:this.config,sessionContext:h,workspaceFiles:c,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(s,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},l=y(g),m=y({...g,mode:"minimal"});de.debug(`[${s}] System prompt (${l.length} chars): ${this.config.verboseDebugLogs?l:l.slice(0,15)+"..."}`);try{const n=await this.agentService.sendMessage(s,r,o.sessionId,l,m,a,this.getChatSetting(s,"coderSkill")??this.coderSkill,this.getChatSetting(s,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(s,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(s,"sandboxEnabled")??!1);if(n.sessionReset){if("[AGENT_CLOSED]"===n.response)return de.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),"";{const i=n.response||"Session corruption detected",r={sessionKey:s,sessionId:o.sessionId,error:new Error(i),timestamp:new Date},a=le.analyzeError(r.error,r),h=le.getRecoveryStrategy(a);return de.warn(`[${s}] ${h.message} (error: ${i})`),this.sessionManager.updateSessionId(s,""),h.clearSession&&(this.agentService.destroySession(s),this.memoryManager&&this.memoryManager.clearSession(s)),"clear_and_retry"!==h.action||t?"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one.":(de.info(`[${s}] Retrying with fresh session after: ${i}`),this.handleMessage(e,!0))}}if(de.debug(`[${s}] Response from agent (session=${n.sessionId}, len=${n.response.length}): ${this.config.verboseDebugLogs?n.response:n.response.slice(0,15)+"..."}`),n.sessionId&&this.sessionManager.updateSessionId(s,n.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(r.text||"[media]").replace(/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) \d{4}-\d{2}-\d{2} \d{2}:\d{2}\]\n?/,"").trim();await this.memoryManager.append(s,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(s,"assistant",ne(n.fullResponse??n.response))}if("max_turns"===n.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return n.response?n.response+e:e.trim()}if("max_budget"===n.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return n.response?n.response+e:e.trim()}if("refusal"===n.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return n.response?n.response+e:e.trim()}if("max_tokens"===n.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return n.response?n.response+e:e.trim()}return n.response}catch(e){const t=e instanceof Error?e.message:String(e);return t.includes("SessionAgent closed")||t.includes("agent closed")?(de.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),""):(de.error(`Agent error for ${s}: ${e}`),`Error: ${t}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){de.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),this.config.channels.mesh.enabled&&e.push("mesh"),de.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{de.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.nodeRegistry.startPingLoop(),this.startAutoRenewTimer(),de.info("Server started successfully"),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{})}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),de.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),de.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?de.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?de.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):de.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}getMeshChannel(){return this.meshChannel}async triggerRestart(){de.info("Trigger restart requested");const e=oe();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();de.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),await this.channelManager.clearTyping(t.channel,t.chatId),de.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){de.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(de.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>de.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){de.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),o=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(o,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){de.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}de.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){de.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new u(this.sessionDb),e.memory.enabled?this.memoryManager=new M(e.memoryDir,e.timezone):this.memoryManager=null;const t=C(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new R(t,s),this.commandRegistry=new A,this.setupCommands(),this.channelManager=new c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsFactory=()=>z(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.stopMemorySearch(),this.createMemorySearch(e),await this.browserService.reconfigure(e.browser);const n=o(e.agent.workspacePath,".plasma"),i=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new p(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>G(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>q(()=>this.config):void 0,this.memorySearch?()=>V(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>J({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,i?()=>X({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=w(this.config.dataDir);return y({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>Y({plasmaRootDir:n,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Z(this.conceptStore):void 0),S(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),de.info("Server reconfigured successfully")}async stop(){de.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),this.conceptStore&&this.conceptStore.close(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),de.info("Server stopped")}}
1
+ import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as o,resolve as r}from"node:path";import{TokenDB as i}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as h}from"./agent/session-db.js";import{ChannelManager as c}from"./gateway/channel-manager.js";import{TypingCoordinator as m}from"./gateway/typing-coordinator.js";import{TelegramChannel as g}from"./gateway/channels/telegram/index.js";import{WhatsAppChannel as l}from"./gateway/channels/whatsapp.js";import{WebChatChannel as d}from"./gateway/channels/webchat.js";import{MeshChannel as p}from"./gateway/channels/mesh.js";import{ResponsesChannel as f}from"./channels/responses.js";import{AgentService as u}from"./agent/agent-service.js";import{SessionManager as y}from"./agent/session-manager.js";import{buildPrompt as b,buildSystemPrompt as S}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as w,loadWorkspaceFiles as v}from"./agent/workspace-files.js";import{NodeRegistry as M}from"./gateway/node-registry.js";import{MemoryManager as R}from"./memory/memory-manager.js";import{MessageProcessor as C}from"./media/message-processor.js";import{loadSTTProvider as A}from"./stt/stt-loader.js";import{CommandRegistry as $}from"./commands/command-registry.js";import{NewCommand as T}from"./commands/new.js";import{CompactCommand as k}from"./commands/compact.js";import{ModelCommand as j,DefaultModelCommand as x}from"./commands/model.js";import{StopCommand as I}from"./commands/stop.js";import{HelpCommand as D}from"./commands/help.js";import{McpCommand as E}from"./commands/mcp.js";import{ModelsCommand as _}from"./commands/models.js";import{CoderCommand as P}from"./commands/coder.js";import{SandboxCommand as N}from"./commands/sandbox.js";import{SubAgentsCommand as F}from"./commands/subagents.js";import{CustomSubAgentsCommand as U}from"./commands/customsubagents.js";import{StatusCommand as K}from"./commands/status.js";import{ShowToolCommand as O}from"./commands/showtool.js";import{UsageCommand as H}from"./commands/usage.js";import{DebugA2UICommand as L}from"./commands/debuga2ui.js";import{DebugDynamicCommand as B}from"./commands/debugdynamic.js";import{CronService as z}from"./cron/cron-service.js";import{stripHeartbeatToken as Q,isHeartbeatContentEffectivelyEmpty as W}from"./cron/heartbeat-token.js";import{createServerToolsServer as q}from"./tools/server-tools.js";import{createCronToolsServer as G}from"./tools/cron-tools.js";import{createTTSToolsServer as V}from"./tools/tts-tools.js";import{createMemoryToolsServer as J}from"./tools/memory-tools.js";import{createBrowserToolsServer as X}from"./tools/browser-tools.js";import{createPicoToolsServer as Y}from"./tools/pico-tools.js";import{createPlasmaClientToolsServer as Z}from"./tools/plasma-client-tools.js";import{createConceptToolsServer as ee}from"./tools/concept-tools.js";import{BrowserService as te}from"./browser/browser-service.js";import{MemorySearch as se}from"./memory/memory-search.js";import{ConceptStore as ne}from"./memory/concept-store.js";import{stripMediaLines as oe}from"./utils/media-response.js";import{loadConfig as re,loadRawConfig as ie,backupConfig as ae,resolveModelEntry as he,modelRefName as ce}from"./config.js";import{stringify as me}from"yaml";import{createLogger as ge}from"./utils/logger.js";import{SessionErrorHandler as le}from"./agent/session-error-handler.js";import{initStickerCache as de}from"./gateway/channels/telegram/stickers.js";const pe=ge("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;typingCoordinator;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsFactory;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;conceptStore=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;meshChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new i(e.dbPath),this.sessionDb=new h(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new M,this.sessionManager=new y(this.sessionDb),e.memory.enabled&&(this.memoryManager=new R(e.memoryDir,e.timezone,e.ckr.l2Prefix,e.ckr.removeL2PrefixFromMemory));const t=A(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new C(t,n),de(e.dataDir),this.commandRegistry=new $,this.setupCommands(),this.channelManager=new c(e,this.tokenDb,e=>this.handleMessage(e),e=>this.handleReaction(e)),this.registerChannels(),this.typingCoordinator=new m(this.channelManager),this.serverToolsFactory=()=>q({restartFn:()=>this.triggerRestart(),timezone:this.config.timezone,memoryDir:this.config.memoryDir,warmRestart:this.config.warmRestart}),this.cronService=this.createCronService(),this.createMemorySearch(e),this.browserService=new te,this.conceptStore=new ne(e.dataDir);const r=o(e.dataDir,"CONCEPTS.md");this.conceptStore.importFromTurtleIfEmpty(r),this.memorySearch&&this.conceptStore&&this.memorySearch.setConceptStore(this.conceptStore);const g=o(e.agent.workspacePath,".plasma"),l=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new u(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>G(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>V(()=>this.config):void 0,this.memorySearch?()=>J(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>X({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,l?()=>Y({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=v(this.config.dataDir);return S({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>Z({plasmaRootDir:g,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>ee(this.conceptStore):void 0),w(e.dataDir),s(o(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(o(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(o(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new z({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e),sessionReaper:{pruneStaleSessions:e=>this.sessionDb.pruneStaleCronSessions(e)}})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=he(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new se(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK,l0:e.memory.l0??{enabled:!0,model:""}}),J(this.memorySearch);pe.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const o=`${s}:${n}`;e.has(o)||(e.add(o),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),o=e.sessionKey.substring(t+1);"cron"!==n&&o&&(this.channelManager.getAdapter(n)&&s(n,o))}return t}async executeCronJob(e){const t=this.config.cron.broadcastEvents;if(!t&&!this.channelManager.getAdapter(e.channel))return pe.warn(`Cron job "${e.name}": skipped (channel "${e.channel}" is not active)`),{response:"",delivered:!1};if(e.suppressToken&&"__heartbeat"===e.name){const t=this.triageHeartbeat();if(!t.shouldRun)return pe.info(`Cron job "${e.name}": skipped by triage (${t.reason})`),{response:"",delivered:!1};pe.info(`Cron job "${e.name}": triage passed (${t.reason})`)}const s="boolean"==typeof e.isolated?e.isolated:this.config.cron.isolated,n=s?"cron":e.channel,o=s?e.name:e.chatId;pe.info(`Cron job "${e.name}": session=${n}:${o}, delivery=${e.channel}:${e.chatId}${t?" (broadcast)":""}`);const r={chatId:o,userId:"cron",channelName:n,text:e.message,attachments:[]},i=`${n}:${o}`;try{const s=await this.handleMessage(r);let n=s;if(e.suppressToken){const t=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:o,text:r}=Q(s,t);if(o)return pe.info(`Cron job "${e.name}": response suppressed (HEARTBEAT_OK)`),{response:s,delivered:!1};n=r}if(t){const t=this.collectBroadcastTargets();pe.info(`Cron job "${e.name}": broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,n)));for(const e of t)this.typingCoordinator.release(e.channel,e.chatId)}else await this.channelManager.sendResponse(e.channel,e.chatId,n),this.typingCoordinator.release(e.channel,e.chatId);return{response:n,delivered:!0}}finally{this.agentService.destroySession(i),this.sessionManager.resetSession(i),this.memoryManager&&this.memoryManager.clearSession(i),pe.info(`Cron job "${e.name}": ephemeral session destroyed (${i})`)}}triageHeartbeat(){const t=(new Date).getHours(),s=o(this.config.agent.workspacePath,"attention","pending_signals.md");if(n(s))try{const t=e(s,"utf-8").trim().split("\n").filter(e=>e.trim()&&!e.startsWith("#"));if(t.length>0)return{shouldRun:!0,reason:`${t.length} pending signal(s)`}}catch{}const r=o(this.config.dataDir,"HEARTBEAT.md");let i=!1;if(n(r))try{const t=e(r,"utf-8");i=!W(t)}catch{i=!0}const a=this.sessionDb.hasRecentActivity(3e5);return t>=23||t<7?a?{shouldRun:!0,reason:"night mode but recent messages"}:{shouldRun:!1,reason:"night mode, no activity"}:i||a?{shouldRun:!0,reason:a?"recent messages":"actionable heartbeat"}:{shouldRun:!1,reason:"no signals, no messages, empty heartbeat"}}setupCommands(){this.commandRegistry.register(new T),this.commandRegistry.register(new k);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new j(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),r=this.sessionManager.getModel(e)||this.config.agent.model,i=he(this.config,r),a=o(i?.name??ce(r)),h=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const c=a||h;return c&&this.sessionManager.resetSession(e),c},e)),this.commandRegistry.register(new x(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=r(process.cwd(),"config.yaml"),s=ie(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),ae(e),t(e,me(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new _(()=>this.config.models??[],e)),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new O(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new F(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new U(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new K(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=he(this.config,t),o=s?he(this.config,s):void 0;return{configDefaultModel:ce(this.config.agent.model),agentModel:n?.id??t,agentModelName:n?.name??ce(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?ce(s):void 0),fallbackActive:this.agentService.isFallbackActive(e),coderSkill:this.getChatSetting(e,"coderSkill")??this.coderSkill,showToolUse:this.getChatSetting(e,"showToolUse")??this.showToolUse,subagentsEnabled:this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,customSubAgentsEnabled:this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,sandboxEnabled:this.getChatSetting(e,"sandboxEnabled")??!1,connectedNodes:this.nodeRegistry.listNodes().map(e=>({nodeId:e.nodeId,displayName:e.displayName,hostname:e.hostname}))}})),this.commandRegistry.register(new I(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new E(()=>this.agentService.getToolServers())),this.commandRegistry.register(new D(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new H(e=>this.agentService.getUsage(e))),this.commandRegistry.register(new L(this.nodeRegistry)),this.commandRegistry.register(new B(this.nodeRegistry))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){pe.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,t,this.tokenDb);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new l(s);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new f({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}if(this.config.channels.mesh.enabled){const e=this.config.channels.mesh;e.agentId&&e.privateKey?(this.meshChannel||(this.meshChannel=new p({brokerUrl:e.brokerUrl,agentId:e.agentId,privateKey:e.privateKey,reconnectDelayMs:e.reconnectDelayMs}),this.meshChannel.onReply((e,t,s)=>{pe.info(`[Mesh] Routing reply from ${t} to origin session ${e}`);const n=e.indexOf(":"),o=n>0?e.substring(0,n):"mesh",r=n>0?e.substring(n+1):t,i={chatId:r,userId:t,channelName:o,text:`[Mesh reply from ${t}] ${s}`,attachments:[],username:t};this.handleMessage(i).then(t=>{t&&t.trim()&&this.channelManager.sendResponse(o,r,t).catch(t=>{pe.error(`[Mesh] Failed to deliver reply-response to ${e}: ${t}`)})}).catch(s=>{pe.error(`[Mesh] Failed to process reply from ${t} in session ${e}: ${s}`)})})),this.channelManager.registerAdapter(this.meshChannel)):pe.warn("Mesh channel enabled but agentId or privateKey not configured")}this.webChatChannel||(this.webChatChannel=new d),this.channelManager.registerAdapter(this.webChatChannel)}handleReaction(e){if(!this.memoryManager)return;const t=`${e.channelName}:${e.chatId}`;this.memoryManager.appendReaction(t,e.emoji,e.username??e.userId,e.removed).catch(e=>{pe.error(`Failed to append reaction to memory: ${e}`)})}async handleMessage(e,t=!1){const s=`${e.channelName}:${e.chatId}`,n=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";pe.info(`Message from ${s} (user=${e.userId}, ${e.username??"?"}): ${n}`),this.config.verboseDebugLogs&&pe.debug(`Message from ${s} full text: ${e.text??"[no text]"}`);try{if(this.typingCoordinator.acquire(e.channelName,e.chatId),e.text){if(e.text.startsWith("__ask:")){const t=e.text.substring(6);return this.agentService.resolveQuestion(s,t),""}if(this.agentService.hasPendingQuestion(s)){const t=e.text.trim();return this.agentService.resolveQuestion(s,t),`Selected: ${t}`}if(e.text.startsWith("__tool_perm:")){const t="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(s,t),t?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(s)){const t=e.text.trim().toLowerCase();if("approve"===t||"approva"===t)return this.agentService.resolvePermission(s,!0),"Tool approved.";if("deny"===t||"vieta"===t||"blocca"===t)return this.agentService.resolvePermission(s,!1),"Tool denied."}}const n=!0===e.__passthrough;if(!n&&e.text&&this.commandRegistry.isCommand(e.text)){const t=await this.commandRegistry.dispatch(e.text,{sessionKey:s,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(t)return t.passthrough?this.handleMessage({...e,text:t.passthrough,__passthrough:!0}):(t.resetSession?(this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s)):t.resetAgent&&this.agentService.destroySession(s),t.buttons&&t.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,t.text,t.buttons),""):t.text)}if(!n&&e.text?.startsWith("/"))return this.agentService.isBusy(s)?"I'm busy right now. Please resend this request later.":this.handleMessage({...e,text:e.text,__passthrough:!0});const o=this.sessionManager.getOrCreate(s),r=await this.messageProcessor.process(e),i=b(r,void 0,{sessionKey:s,channel:e.channelName,chatId:e.chatId},this.config.timezone,this.config.ckr.applyL2PrefixOnEachUserMessage?this.config.ckr.l2Prefix:void 0);pe.debug(`[${s}] Prompt to agent (${i.text.length} chars): ${this.config.verboseDebugLogs?i.text:i.text.slice(0,15)+"..."}${i.images.length>0?` [+${i.images.length} image(s)]`:""}`);const a=o.model,h={sessionKey:s,channel:e.channelName,chatId:e.chatId,sessionId:o.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(s):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(s):""},c=v(this.config.dataDir),m={config:this.config,sessionContext:h,workspaceFiles:c,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(s,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},g=S(m),l=S({...m,mode:"minimal"});pe.debug(`[${s}] System prompt (${g.length} chars): ${this.config.verboseDebugLogs?g:g.slice(0,15)+"..."}`);try{const n=await this.agentService.sendMessage(s,i,o.sessionId,g,l,a,this.getChatSetting(s,"coderSkill")??this.coderSkill,this.getChatSetting(s,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(s,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(s,"sandboxEnabled")??!1);if(n.sessionReset){if("[AGENT_CLOSED]"===n.response)return pe.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),"";{const r=n.response||"Session corruption detected",i={sessionKey:s,sessionId:o.sessionId,error:new Error(r),timestamp:new Date},a=le.analyzeError(i.error,i),h=le.getRecoveryStrategy(a);return pe.warn(`[${s}] ${h.message} (error: ${r})`),this.sessionManager.updateSessionId(s,""),h.clearSession&&(this.agentService.destroySession(s),this.memoryManager&&this.memoryManager.clearSession(s)),"clear_and_retry"!==h.action||t?"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one.":(pe.info(`[${s}] Retrying with fresh session after: ${r}`),this.handleMessage(e,!0))}}pe.debug(`[${s}] Response from agent (session=${n.sessionId}, len=${n.response.length}): ${this.config.verboseDebugLogs?n.response:n.response.slice(0,15)+"..."}`),n.sessionId&&this.sessionManager.updateSessionId(s,n.sessionId);const h="cron"===e.userId,c=!0===this.config.cron.trackMemory;if(this.memoryManager&&(!h||c)){const e=(i.text||"[media]").replace(/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) \d{4}-\d{2}-\d{2} \d{2}:\d{2}\]\n?/,"").trim();await this.memoryManager.append(s,"user",e,r.savedFiles.length>0?r.savedFiles:void 0),await this.memoryManager.append(s,"assistant",oe(n.fullResponse??n.response))}if("max_turns"===n.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return n.response?n.response+e:e.trim()}if("max_budget"===n.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return n.response?n.response+e:e.trim()}if("refusal"===n.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return n.response?n.response+e:e.trim()}if("max_tokens"===n.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return n.response?n.response+e:e.trim()}return n.response}catch(e){const t=e instanceof Error?e.message:String(e);return t.includes("SessionAgent closed")||t.includes("agent closed")?(pe.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),""):(pe.error(`Agent error for ${s}: ${e}`),`Error: ${t}`)}}finally{this.typingCoordinator.release(e.channelName,e.chatId)}}async start(){pe.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),this.config.channels.mesh.enabled&&e.push("mesh"),pe.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{}),await this.browserService.start(this.config.browser).catch(e=>{pe.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.nodeRegistry.startPingLoop(),this.startAutoRenewTimer(),pe.info("Server started successfully")}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),pe.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),pe.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?pe.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?pe.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):pe.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}getMeshChannel(){return this.meshChannel}async triggerRestart(){pe.info("Trigger restart requested");const e=re();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();pe.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),this.typingCoordinator.reinforceIfActive(t.channel,t.chatId),pe.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){pe.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(pe.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>pe.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){pe.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),o=s.substring(n+1),r=this.channelManager.getAdapter(t);if(r)try{await r.sendText(o,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){pe.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}pe.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){pe.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new y(this.sessionDb),e.memory.enabled?this.memoryManager=new R(e.memoryDir,e.timezone,e.ckr.l2Prefix):this.memoryManager=null;const t=A(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new C(t,s),this.commandRegistry=new $,this.setupCommands(),this.channelManager=new c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.typingCoordinator=new m(this.channelManager),this.agentService.destroyAll(),this.serverToolsFactory=()=>q({restartFn:()=>this.triggerRestart(),timezone:this.config.timezone,memoryDir:this.config.memoryDir,warmRestart:this.config.warmRestart}),this.cronService=this.createCronService(),this.stopMemorySearch(),this.createMemorySearch(e),this.memorySearch&&this.conceptStore&&this.memorySearch.setConceptStore(this.conceptStore),await this.browserService.reconfigure(e.browser);const n=o(e.agent.workspacePath,".plasma"),r=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new u(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>G(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>V(()=>this.config):void 0,this.memorySearch?()=>J(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>X({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,r?()=>Y({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=v(this.config.dataDir);return S({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>Z({plasmaRootDir:n,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>ee(this.conceptStore):void 0),w(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),pe.info("Server reconfigured successfully")}async stop(){pe.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),this.conceptStore&&this.conceptStore.close(),await this.browserService.stop(),this.typingCoordinator.stopAll(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),pe.info("Server stopped")}}
@@ -1 +1 @@
1
- import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as o}from"../utils/logger.js";const r=o("CronTools");export function createCronToolsServer(o,s){return e({name:"cron-tools",version:"1.0.0",tools:[t("cron_add","Create a new cron job. The job will send the specified message to the agent on the given schedule. Channel and chatId are taken from the current session (from <session_info>). The isolated setting is inherited from the global cron config.",{name:n.string().describe("Unique name for the job (e.g. 'daily-reminder')"),description:n.string().optional().describe("Optional description of what the job does"),message:n.string().describe("The message text to send to the agent when the job fires"),channel:n.string().describe("The channel to deliver responses to (from <session_info>)"),chatId:n.string().describe("The chat ID to deliver responses to (from <session_info>)"),scheduleKind:n.enum(["every","cron","at"]).describe("Schedule type: 'every' for interval, 'cron' for cron expression, 'at' for one-shot"),everyMs:n.number().optional().describe("Interval in milliseconds (required when scheduleKind is 'every')"),cronExpr:n.string().optional().describe("Cron expression like '0 9 * * *' (required when scheduleKind is 'cron')"),at:n.string().optional().describe("ISO datetime for one-shot execution (required when scheduleKind is 'at')"),suppressToken:n.boolean().optional().describe("If true, suppress HEARTBEAT_OK responses (default: false)"),timeoutSeconds:n.number().optional().describe("Per-job execution timeout in seconds. 0 = no timeout. Default: 600 (10 min)")},async e=>{try{const t=s();let n;if("every"===e.scheduleKind){if(!e.everyMs)return{content:[{type:"text",text:"everyMs is required for 'every' schedule"}],isError:!0};n={kind:"every",everyMs:e.everyMs}}else if("cron"===e.scheduleKind){if(!e.cronExpr)return{content:[{type:"text",text:"cronExpr is required for 'cron' schedule"}],isError:!0};n={kind:"cron",expr:e.cronExpr}}else{if(!e.at)return{content:[{type:"text",text:"at is required for 'at' schedule"}],isError:!0};n={kind:"at",at:e.at}}const i=await o.add({name:e.name,description:e.description,channel:e.channel,chatId:e.chatId,message:e.message,schedule:n,isolated:t.cron.isolated,suppressToken:e.suppressToken??!1,timeoutSeconds:e.timeoutSeconds,enabled:!0});return r.info(`Cron job created by agent: "${i.name}" (${i.id})`),{content:[{type:"text",text:`Job "${i.name}" created (id: ${i.id}). Schedule: ${JSON.stringify(n)}. Isolated: ${t.cron.isolated}. Next run: ${i.state.nextRunAtMs?new Date(i.state.nextRunAtMs).toISOString():"none"}.`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return r.error(`cron_add failed: ${t}`),{content:[{type:"text",text:`Failed to create job: ${t}`}],isError:!0}}}),t("cron_list","List all cron jobs with their status, schedule, and next run time.",{includeDisabled:n.boolean().optional().describe("Include disabled jobs (default: false)")},async e=>{try{const t=await o.list({includeDisabled:e.includeDisabled??!1});if(0===t.length)return{content:[{type:"text",text:"No cron jobs found."}]};const n=t.map(e=>({id:e.id,name:e.name,enabled:e.enabled,isolated:e.isolated,schedule:e.schedule,channel:e.channel,chatId:e.chatId,nextRun:e.state.nextRunAtMs?new Date(e.state.nextRunAtMs).toISOString():null,lastStatus:e.state.lastStatus??null}));return{content:[{type:"text",text:JSON.stringify(n,null,2)}]}}catch(e){return{content:[{type:"text",text:`Failed to list jobs: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),t("cron_remove","Delete a cron job by its ID.",{id:n.string().describe("The job ID to delete")},async e=>{try{return(await o.remove(e.id)).removed?(r.info(`Cron job removed by agent: ${e.id}`),{content:[{type:"text",text:`Job ${e.id} deleted.`}]}):{content:[{type:"text",text:`Job ${e.id} not found.`}],isError:!0}}catch(e){return{content:[{type:"text",text:`Failed to delete job: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),t("cron_update","Update an existing cron job. Only provide the fields you want to change.",{id:n.string().describe("The job ID to update"),name:n.string().optional().describe("New name for the job"),description:n.string().optional().describe("New description"),message:n.string().optional().describe("New message text"),enabled:n.boolean().optional().describe("Enable or disable the job"),suppressToken:n.boolean().optional().describe("Enable or disable HEARTBEAT_OK suppression"),timeoutSeconds:n.number().optional().describe("Per-job execution timeout in seconds. 0 = no timeout. Default: 600 (10 min)")},async e=>{try{const t={};void 0!==e.name&&(t.name=e.name),void 0!==e.description&&(t.description=e.description),void 0!==e.message&&(t.message=e.message),void 0!==e.enabled&&(t.enabled=e.enabled),void 0!==e.suppressToken&&(t.suppressToken=e.suppressToken),void 0!==e.timeoutSeconds&&(t.timeoutSeconds=e.timeoutSeconds);const n=await o.update(e.id,t);return r.info(`Cron job updated by agent: "${n.name}" (${n.id})`),{content:[{type:"text",text:`Job "${n.name}" updated. Enabled: ${n.enabled}. Next run: ${n.state.nextRunAtMs?new Date(n.state.nextRunAtMs).toISOString():"none"}.`}]}}catch(e){return{content:[{type:"text",text:`Failed to update job: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}})]})}
1
+ import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as o}from"../utils/logger.js";const r=o("CronTools");export function createCronToolsServer(o,s){return e({name:"cron-tools",version:"1.0.0",tools:[t("cron_add","Create a new cron job. The job will send the specified message to the agent on the given schedule. Channel and chatId are taken from the current session (from <session_info>). The isolated setting is inherited from the global cron config.",{name:n.string().describe("Unique name for the job (e.g. 'daily-reminder')"),description:n.string().optional().describe("Optional description of what the job does"),message:n.string().describe("The message text to send to the agent when the job fires"),channel:n.string().describe("The channel to deliver responses to (from <session_info>)"),chatId:n.string().describe("The chat ID to deliver responses to (from <session_info>)"),scheduleKind:n.enum(["every","cron","at"]).describe("Schedule type: 'every' for interval, 'cron' for cron expression, 'at' for one-shot"),everyMs:n.coerce.number().optional().describe("Interval in milliseconds (required when scheduleKind is 'every')"),cronExpr:n.string().optional().describe("Cron expression like '0 9 * * *' (required when scheduleKind is 'cron')"),at:n.string().optional().describe("ISO datetime for one-shot execution (required when scheduleKind is 'at')"),suppressToken:n.boolean().optional().describe("If true, suppress HEARTBEAT_OK responses (default: false)"),timeoutSeconds:n.coerce.number().optional().describe("Per-job execution timeout in seconds. 0 = no timeout. Default: 600 (10 min)")},async e=>{try{const t=s();let n;if("every"===e.scheduleKind){if(!e.everyMs)return{content:[{type:"text",text:"everyMs is required for 'every' schedule"}],isError:!0};n={kind:"every",everyMs:e.everyMs}}else if("cron"===e.scheduleKind){if(!e.cronExpr)return{content:[{type:"text",text:"cronExpr is required for 'cron' schedule"}],isError:!0};n={kind:"cron",expr:e.cronExpr}}else{if(!e.at)return{content:[{type:"text",text:"at is required for 'at' schedule"}],isError:!0};n={kind:"at",at:e.at}}const i=await o.add({name:e.name,description:e.description,channel:e.channel,chatId:e.chatId,message:e.message,schedule:n,isolated:t.cron.isolated,suppressToken:e.suppressToken??!1,timeoutSeconds:e.timeoutSeconds,enabled:!0});return r.info(`Cron job created by agent: "${i.name}" (${i.id})`),{content:[{type:"text",text:`Job "${i.name}" created (id: ${i.id}). Schedule: ${JSON.stringify(n)}. Isolated: ${t.cron.isolated}. Next run: ${i.state.nextRunAtMs?new Date(i.state.nextRunAtMs).toISOString():"none"}.`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return r.error(`cron_add failed: ${t}`),{content:[{type:"text",text:`Failed to create job: ${t}`}],isError:!0}}}),t("cron_list","List all cron jobs with their status, schedule, and next run time.",{includeDisabled:n.boolean().optional().describe("Include disabled jobs (default: false)")},async e=>{try{const t=await o.list({includeDisabled:e.includeDisabled??!1});if(0===t.length)return{content:[{type:"text",text:"No cron jobs found."}]};const n=t.map(e=>({id:e.id,name:e.name,enabled:e.enabled,isolated:e.isolated,schedule:e.schedule,channel:e.channel,chatId:e.chatId,nextRun:e.state.nextRunAtMs?new Date(e.state.nextRunAtMs).toISOString():null,lastStatus:e.state.lastStatus??null}));return{content:[{type:"text",text:JSON.stringify(n,null,2)}]}}catch(e){return{content:[{type:"text",text:`Failed to list jobs: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),t("cron_remove","Delete a cron job by its ID.",{id:n.string().describe("The job ID to delete")},async e=>{try{return(await o.remove(e.id)).removed?(r.info(`Cron job removed by agent: ${e.id}`),{content:[{type:"text",text:`Job ${e.id} deleted.`}]}):{content:[{type:"text",text:`Job ${e.id} not found.`}],isError:!0}}catch(e){return{content:[{type:"text",text:`Failed to delete job: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),t("cron_update","Update an existing cron job. Only provide the fields you want to change.",{id:n.string().describe("The job ID to update"),name:n.string().optional().describe("New name for the job"),description:n.string().optional().describe("New description"),message:n.string().optional().describe("New message text"),enabled:n.boolean().optional().describe("Enable or disable the job"),suppressToken:n.boolean().optional().describe("Enable or disable HEARTBEAT_OK suppression"),timeoutSeconds:n.coerce.number().optional().describe("Per-job execution timeout in seconds. 0 = no timeout. Default: 600 (10 min)")},async e=>{try{const t={};void 0!==e.name&&(t.name=e.name),void 0!==e.description&&(t.description=e.description),void 0!==e.message&&(t.message=e.message),void 0!==e.enabled&&(t.enabled=e.enabled),void 0!==e.suppressToken&&(t.suppressToken=e.suppressToken),void 0!==e.timeoutSeconds&&(t.timeoutSeconds=e.timeoutSeconds);const n=await o.update(e.id,t);return r.info(`Cron job updated by agent: "${n.name}" (${n.id})`),{content:[{type:"text",text:`Job "${n.name}" updated. Enabled: ${n.enabled}. Next run: ${n.state.nextRunAtMs?new Date(n.state.nextRunAtMs).toISOString():"none"}.`}]}}catch(e){return{content:[{type:"text",text:`Failed to update job: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}})]})}
@@ -1 +1 @@
1
- import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as r}from"zod";import{createLogger as n}from"../utils/logger.js";const s=n("MemoryTools");export function createMemoryToolsServer(n){return e({name:"memory-tools",version:"1.0.0",tools:[t("memory_search","Search conversation memory using hybrid keyword + semantic search. Returns matching chunks from past conversations with relevance scores. Results include chunkId — use memory_expand to get full chunk content when snippets are insufficient. Formulate clear, specific queries. For broad searches, call multiple times with different angles or phrasings.",{query:r.string().describe("The search query — be specific and descriptive for best results"),maxResults:r.number().optional().describe("Maximum number of results to return (default 6)")},async e=>{try{const t=await n.search(e.query,e.maxResults);if(0===t.length)return{content:[{type:"text",text:"No matching memories found."}]};const r=n.getMaxInjectedChars();let o=JSON.stringify(t,null,2);if(r>0&&o.length>r){const e=[...t];for(;e.length>1&&(e.pop(),o=JSON.stringify(e,null,2),!(o.length<=r)););o.length>r&&(o=o.slice(0,r)+"\n... [truncated — result exceeded maxInjectedChars limit]"),s.info(`memory_search: trimmed from ${t.length} to ${e.length} results to fit maxInjectedChars=${r}`)}return{content:[{type:"text",text:o}]}}catch(e){const t=e instanceof Error?e.message:String(e);return s.error(`memory_search error: ${t}`),{content:[{type:"text",text:`Search error: ${t}`}],isError:!0}}}),t("memory_expand","Get the full content of one or more memory chunks by their IDs (from memory_search results). Use this when a search snippet or L0 abstract is too short and you need the complete text.",{ids:r.array(r.number()).describe("Array of chunk IDs from memory_search results (chunkId field)")},async e=>{try{const t=n.expandChunks(e.ids);return 0===t.length?{content:[{type:"text",text:"No chunks found for the given IDs."}]}:{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}catch(e){const t=e instanceof Error?e.message:String(e);return s.error(`memory_expand error: ${t}`),{content:[{type:"text",text:`Expand error: ${t}`}],isError:!0}}}),t("memory_get","Read a memory file (full or a specific slice) to get surrounding context after a search hit. Use the path from memory_search results.",{path:r.string().describe("Relative path to the memory file (from memory_search results)"),from:r.number().optional().describe("Starting line number (1-based). Omit to read from the beginning."),lines:r.number().optional().describe("Number of lines to read. Omit to read the entire file.")},async e=>{try{const t=n.readFile(e.path,e.from,e.lines);return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}catch(e){const t=e instanceof Error?e.message:String(e);return s.error(`memory_get error: ${t}`),{content:[{type:"text",text:`Read error: ${t}`}],isError:!0}}}),t("memory_list","List memory files within a date range. Returns relative paths (compatible with memory_get) for all .md files whose filename starts with a date between `from` and `to` (inclusive). Searches across all channels and subfolders.",{from:r.string().describe("Start date (inclusive), format YYYY-MM-DD"),to:r.string().describe("End date (inclusive), format YYYY-MM-DD")},async e=>{try{const t=n.listFiles(e.from,e.to);return 0===t.length?{content:[{type:"text",text:"No memory files found in the given date range."}]}:{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}catch(e){const t=e instanceof Error?e.message:String(e);return s.error(`memory_list error: ${t}`),{content:[{type:"text",text:`List error: ${t}`}],isError:!0}}}),t("memory_record_access","Record that memory chunks were accessed/used in a response. This improves future retrieval by boosting frequently-used chunks. Automatically called by memory_expand, but can also be called manually for chunks from memory_search that were used without expanding.",{ids:r.array(r.number()).describe("Array of chunk IDs that were used")},async e=>{try{return n.recordAccess(e.ids),{content:[{type:"text",text:`Recorded access for ${e.ids.length} chunks.`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return s.error(`memory_record_access error: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}})]})}
1
+ import{createSdkMcpServer as e,tool as r}from"@anthropic-ai/claude-agent-sdk";import{z as t}from"zod";import{createLogger as n}from"../utils/logger.js";const s=n("MemoryTools"),o=t.preprocess(e=>"string"==typeof e?Number(e):e,t.number()),c=t.preprocess(e=>null==e?void 0:"string"==typeof e?Number(e):e,t.number().optional()),i=t.preprocess(e=>{if("string"==typeof e)try{return JSON.parse(e)}catch{return e}return e},t.array(t.preprocess(e=>"string"==typeof e?Number(e):e,t.number())));export function createMemoryToolsServer(n){return e({name:"memory-tools",version:"1.0.0",tools:[r("memory_search","Search conversation memory using hybrid keyword + semantic search. Returns matching chunks from past conversations with relevance scores. Results include chunkId — use memory_expand to get full chunk content when snippets are insufficient. Formulate clear, specific queries. For broad searches, call multiple times with different angles or phrasings.",{query:t.string().describe("The search query — be specific and descriptive for best results"),maxResults:c.describe("Maximum number of results to return (default 6)")},async e=>{try{const r=await n.search(e.query,e.maxResults);if(0===r.length)return{content:[{type:"text",text:"No matching memories found."}]};const t=n.getMaxInjectedChars();let o=JSON.stringify(r,null,2);if(t>0&&o.length>t){const e=[...r];for(;e.length>1&&(e.pop(),o=JSON.stringify(e,null,2),!(o.length<=t)););o.length>t&&(o=o.slice(0,t)+"\n... [truncated — result exceeded maxInjectedChars limit]"),s.info(`memory_search: trimmed from ${r.length} to ${e.length} results to fit maxInjectedChars=${t}`)}return{content:[{type:"text",text:o}]}}catch(e){const r=e instanceof Error?e.message:String(e);return s.error(`memory_search error: ${r}`),{content:[{type:"text",text:`Search error: ${r}`}],isError:!0}}}),r("memory_expand","Get the full content of one or more memory chunks by their IDs (from memory_search results). Use this when a search snippet or L0 abstract is too short and you need the complete text.",{ids:i.describe("Array of chunk IDs from memory_search results (chunkId field)")},async e=>{try{const r=n.expandChunks(e.ids);return 0===r.length?{content:[{type:"text",text:"No chunks found for the given IDs."}]}:{content:[{type:"text",text:JSON.stringify(r,null,2)}]}}catch(e){const r=e instanceof Error?e.message:String(e);return s.error(`memory_expand error: ${r}`),{content:[{type:"text",text:`Expand error: ${r}`}],isError:!0}}}),r("memory_get","Read a memory file (full or a specific slice) to get surrounding context after a search hit. Use the path from memory_search results.",{path:t.string().describe("Relative path to the memory file (from memory_search results)"),from:c.describe("Starting line number (1-based). Omit to read from the beginning."),lines:c.describe("Number of lines to read. Omit to read the entire file.")},async e=>{try{const r=n.readFile(e.path,e.from,e.lines);return{content:[{type:"text",text:JSON.stringify(r,null,2)}]}}catch(e){const r=e instanceof Error?e.message:String(e);return s.error(`memory_get error: ${r}`),{content:[{type:"text",text:`Read error: ${r}`}],isError:!0}}}),r("memory_list","List memory files within a date range. Returns relative paths (compatible with memory_get) for all .md files whose filename starts with a date between `from` and `to` (inclusive). Searches across all channels and subfolders.",{from:t.string().describe("Start date (inclusive), format YYYY-MM-DD"),to:t.string().describe("End date (inclusive), format YYYY-MM-DD")},async e=>{try{const r=n.listFiles(e.from,e.to);return 0===r.length?{content:[{type:"text",text:"No memory files found in the given date range."}]}:{content:[{type:"text",text:JSON.stringify(r,null,2)}]}}catch(e){const r=e instanceof Error?e.message:String(e);return s.error(`memory_list error: ${r}`),{content:[{type:"text",text:`List error: ${r}`}],isError:!0}}}),r("memory_record_access","Record that memory chunks were accessed/used in a response. This improves future retrieval by boosting frequently-used chunks. Automatically called by memory_expand, but can also be called manually for chunks from memory_search that were used without expanding.",{ids:i.describe("Array of chunk IDs that were used")},async e=>{try{return n.recordAccess(e.ids),{content:[{type:"text",text:`Recorded access for ${e.ids.length} chunks.`}]}}catch(e){const r=e instanceof Error?e.message:String(e);return s.error(`memory_record_access error: ${r}`),{content:[{type:"text",text:`Error: ${r}`}],isError:!0}}}),r("memory_get_recent_chunks","Get recently accessed memory chunks for importance re-scoring. Returns chunks accessed in the last N days with their content snippet, current importance, and access count. Used by dreaming for LLM-based importance refinement.",{days:c.describe("Number of days to look back (default 7)"),limit:c.describe("Maximum number of chunks to return (default 100)")},async e=>{try{const r=n.getRecentlyAccessedChunks(e.days,e.limit);return{content:[{type:"text",text:JSON.stringify(r,null,2)}]}}catch(e){return{content:[{type:"text",text:`Error: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),r("memory_update_importance","Update importance scores for specific memory chunks. Importance is 1-10 (1=filler, 5=neutral, 9-10=critical). Used by dreaming for LLM-based importance refinement after reviewing chunk content.",{updates:t.preprocess(e=>{if("string"==typeof e)try{return JSON.parse(e)}catch{return e}return e},t.array(t.object({chunkId:o.describe("Chunk ID"),importance:t.preprocess(e=>"string"==typeof e?Number(e):e,t.number().min(1).max(10)).describe("New importance score (1-10)")}))).describe("Array of chunk ID + importance pairs")},async e=>{try{return{content:[{type:"text",text:`Updated importance for ${n.updateImportance(e.updates)}/${e.updates.length} chunks.`}]}}catch(e){return{content:[{type:"text",text:`Error: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}})]})}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Operational Context Tools — CKR (Contextual Knowledge Retrieval) system.
3
+ *
4
+ * Provides a single tool `build_operational_context` that matches caller-supplied
5
+ * keywords against a YAML rule file (`message-rules.yaml`) and returns matching
6
+ * operational context snippets ordered by relevance score.
7
+ *
8
+ * The rule file is cached in memory and only re-read when its mtime changes.
9
+ * Fail-open: missing or malformed files produce empty results, never errors.
10
+ *
11
+ * Storage: {dataDir}/message-rules.yaml
12
+ */
13
+ /**
14
+ * Load tool rules from data/tool-rules.yaml.
15
+ * Returns a map of tool_name → array of rule strings.
16
+ * Cached with mtime check. Fail-open: returns empty object on error.
17
+ */
18
+ export declare function loadToolRules(dataDir: string): Record<string, string[]>;
19
+ export declare function createOperationalContextToolsServer(dataDir: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
20
+ //# sourceMappingURL=operational-context-tools.d.ts.map
@@ -0,0 +1 @@
1
+ import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as r}from"zod";import{readFileSync as o,statSync as n}from"fs";import{join as s}from"path";import{parse as i}from"yaml";import{createLogger as a}from"../utils/logger.js";const l=a("OperationalContextTools");function c(e,t){return!(!e.includes(t)&&!t.includes(e))||e.length>=5&&t.length>=5&&function(e,t){if(e===t)return 1;const r=e.length,o=t.length;if(0===r||0===o)return 0;const n=Math.max(Math.floor(Math.max(r,o)/2)-1,0),s=new Array(r).fill(!1),i=new Array(o).fill(!1);let a=0,l=0;for(let l=0;l<r;l++){const r=Math.max(0,l-n),c=Math.min(l+n+1,o);for(let o=r;o<c;o++)if(!i[o]&&e[l]===t[o]){s[l]=!0,i[o]=!0,a++;break}}if(0===a)return 0;let c=0;for(let o=0;o<r;o++)if(s[o]){for(;!i[c];)c++;e[o]!==t[c]&&l++,c++}const u=(a/r+a/o+(a-l/2)/a)/3;let m=0;for(let n=0;n<Math.min(4,Math.min(r,o))&&e[n]===t[n];n++)m++;return u+.1*m*(1-u)}(e,t)>=.93}let u=null;function m(e){try{const t=n(e).mtimeMs;if(u&&u.mtime===t)return u.rules;const r=o(e,"utf-8"),s=i(r);if(!s||"object"!=typeof s)return u={mtime:t,rules:new Map},u.rules;const a=new Map;for(const[e,t]of Object.entries(s))t&&Array.isArray(t.match)&&"string"==typeof t.inject&&a.set(e,{match:t.match,inject:t.inject,priority:t.priority,when:t.when});return u={mtime:t,rules:a},l.debug(`Loaded ${a.size} rules from message-rules.yaml`),a}catch{return new Map}}let f=null;export function loadToolRules(e){const t=s(e,"tool-rules.yaml");try{const e=n(t).mtimeMs;if(f&&f.mtime===e)return f.rules;const r=o(t,"utf-8"),s=i(r);if(!s||"object"!=typeof s)return f={mtime:e,rules:{}},f.rules;const a={};for(const[e,t]of Object.entries(s))Array.isArray(t)&&(a[e]=t.filter(e=>"string"==typeof e));return f={mtime:e,rules:a},l.debug(`Loaded tool rules for ${Object.keys(a).length} tools`),a}catch{return{}}}export function createOperationalContextToolsServer(o){const n=s(o,"message-rules.yaml");return e({name:"operational-context-tools",version:"1.0.0",tools:[t("build_operational_context","Match keywords against operational rules and return relevant context snippets. Use this to retrieve situational instructions and guidelines based on the current conversation topic.",{keywords:r.array(r.string()).describe("Keywords to match against operational rules").optional(),ruleIds:r.array(r.string()).describe("Specific rule IDs to retrieve (bypasses keyword matching). Use after Haiku classification.").optional()},async({keywords:e,ruleIds:t})=>{const r=m(n);if(0===r.size)return{content:[{type:"text",text:"No matching operational rules."}]};let o=[];if(t&&t.length>0){for(const e of t){const t=r.get(e);t&&o.push({id:e,inject:t.inject,score:100+(t.priority??0)})}l.debug(`Direct lookup: ${o.length}/${t.length} rules found`)}else{if(!(e&&e.length>0))return{content:[{type:"text",text:"No matching operational rules. Provide keywords or ruleIds."}]};{const t=e.map(e=>e.toLowerCase());r.forEach((e,r)=>{let n=0;for(const r of e.match){const e=r.toLowerCase();t.some(t=>c(t,e))&&n++}if(n>0){const t=10*n+(e.priority??0);o.push({id:r,inject:e.inject,score:t})}}),l.debug(`Keyword match: ${o.length} rules for: ${e.join(", ")}`)}}if(0===o.length)return{content:[{type:"text",text:"No matching operational rules."}]};o.sort((e,t)=>t.score-e.score);return{content:[{type:"text",text:o.map(e=>`[${e.id}] ${e.inject}`).join("\n")}]}}),t("get_ckr_classifier_prompt","Generate the Haiku classifier prompt from current message-rules.yaml. Use this to get an up-to-date classifier prompt for Agent(model=haiku) classification. Call once per session, cache the result.",{},async()=>{const e=m(n);if(0===e.size)return{content:[{type:"text",text:"No rules loaded."}]};return{content:[{type:"text",text:`You are a message classifier. Given a user message, return ONLY a JSON array of matching rule IDs. No explanation, no markdown, no code fences.\n\nRules:\n${Array.from(e.entries()).map(([e,t])=>`- ${e}: ${t.when||`Matches: ${t.match.slice(0,5).join(", ")}`}`).join("\n")}\n\nReturn ONLY a JSON array like ["medical-context"] or []. Nothing else.`}]}})]})}
@@ -1,2 +1,12 @@
1
- export declare function createServerToolsServer(restartFn: () => Promise<void>, timezone?: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
1
+ interface ServerToolsOptions {
2
+ restartFn: () => Promise<void>;
3
+ timezone?: string;
4
+ memoryDir?: string;
5
+ warmRestart?: {
6
+ enabled: boolean;
7
+ pm2Name: string;
8
+ };
9
+ }
10
+ export declare function createServerToolsServer(opts: ServerToolsOptions): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
11
+ export {};
2
12
  //# sourceMappingURL=server-tools.d.ts.map
@@ -1 +1 @@
1
- import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{createLogger as r}from"../utils/logger.js";const n=r("ServerTools");export function createServerToolsServer(r,o){const i=o||Intl.DateTimeFormat().resolvedOptions().timeZone;return e({name:"server-tools",version:"1.0.0",tools:[t("restart_server","Restart the GrabMeABeer server. This reloads config from disk and reinitializes all channels, agents, and services. Active conversations will be interrupted. Use this when you need to apply configuration changes or recover from issues.",{},async()=>(n.info("Server restart triggered by agent"),setTimeout(()=>{r().catch(e=>{const t=e instanceof Error?e.message:String(e);n.error(`Agent-triggered restart failed: ${t}`)})},100),{content:[{type:"text",text:"Server restart initiated. The server will reconfigure momentarily."}]})),t("get_current_time","Get the current date and time in the server's configured timezone. Use this tool whenever you need to know the current time, date, day of the week, or any time-related information. Do not guess or estimate the current time — always call this tool.",{},async()=>{const e=new Date,t=e.toLocaleString("en-GB",{timeZone:i,weekday:"long",year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}),r=e.toISOString();return{content:[{type:"text",text:`Current time (${i}): ${t}\nISO 8601: ${r}\nUnix timestamp: ${Math.floor(e.getTime()/1e3)}`}]}})]})}
1
+ import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{appendFileSync as r,existsSync as o,mkdirSync as n}from"node:fs";import{join as i}from"node:path";import{execSync as s}from"node:child_process";import{createLogger as a}from"../utils/logger.js";const m=a("ServerTools");function c(e,t,s){if(!e)return;const a=i(e,"RESET_HISTORY.md"),c=new Date,d=s?c.toLocaleString("en-GB",{timeZone:s,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}):c.toISOString();o(a)||(n(e,{recursive:!0}),r(a,"# Reset History\n\n| Timestamp | Type |\n|---|---|\n","utf-8")),r(a,`| ${d} | ${t} |\n`,"utf-8"),m.info(`Logged ${t} restart to ${a}`)}export function createServerToolsServer(r){const{restartFn:o,timezone:n,memoryDir:i,warmRestart:a}=r,d=n||Intl.DateTimeFormat().resolvedOptions().timeZone,l=[t("restart_server","Restart the GrabMeABeer server (cold restart). This reloads config from disk and reinitializes all channels, agents, and services. Active conversations will be interrupted. Use this when you need to apply configuration changes or recover from issues.",{},async()=>(m.info("Cold restart triggered by agent"),c(i,"cold",n),setTimeout(()=>{o().catch(e=>{const t=e instanceof Error?e.message:String(e);m.error(`Agent-triggered cold restart failed: ${t}`)})},100),{content:[{type:"text",text:"Server cold restart initiated. The server will reconfigure momentarily."}]})),t("get_current_time","Get the current date and time in the server's configured timezone. Use this tool whenever you need to know the current time, date, day of the week, or any time-related information. Do not guess or estimate the current time — always call this tool.",{},async()=>{const e=new Date,t=e.toLocaleString("en-GB",{timeZone:d,weekday:"long",year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}),r=e.toISOString();return{content:[{type:"text",text:`Current time (${d}): ${t}\nISO 8601: ${r}\nUnix timestamp: ${Math.floor(e.getTime()/1e3)}`}]}})];return a?.enabled&&a.pm2Name&&l.push(t("restart_server_warm","Warm restart the server via PM2. The process is killed and respawned by PM2. Use this when code changes require a full process restart (not just config reload). Requires warmRestart to be enabled in config.",{},async()=>{const e=a.pm2Name;return m.info(`Warm restart (PM2) triggered by agent for process: ${e}`),c(i,"warm",n),setTimeout(()=>{try{s(`pm2 restart ${e}`,{timeout:1e4})}catch(e){const t=e instanceof Error?e.message:String(e);m.error(`PM2 warm restart failed: ${t}`)}},100),{content:[{type:"text",text:`Warm restart initiated via PM2 (process: ${e}). The server will respawn momentarily.`}]}})),e({name:"server-tools",version:"1.0.0",tools:l})}
@@ -157,6 +157,10 @@ Cron: exact timing, isolation, one-shot reminders, heavy work.
157
157
 
158
158
  When multiple skills could handle a task, prefer the one with lower priority number.
159
159
 
160
+ ## Server Restart
161
+
162
+ Restart history is logged in **`memory/RESET_HISTORY.md`** (timestamp + type cold/warm).
163
+
160
164
  ## Make It Yours
161
165
 
162
166
  This is a starting point. Add your own conventions, style, and rules as you figure out what works.
@@ -1,92 +1,111 @@
1
- # BEHAVIOUR.md — Operational rules and lessons learned
2
-
3
- **This file is `BEHAVIOUR.md` in the dataDir.** To add or modify operational rules, edit this file directly.
4
- Optimized for LLM parsing. Each rule: `WHEN trigger → DO action`.
5
- Single source of truth. Dreaming adds here, not elsewhere.
1
+ # BEHAVIOUR.md v2 — Operational Rules
2
+ # Format: each rule has priority (1=critical), trigger, actions
3
+ # Rules are ordered by priority, not by position in file
4
+ # Single source of truth. Dreaming adds here, not elsewhere.
5
+ #
6
+ # HOW TO ADD A NEW RULE:
7
+ # 1. Pick the right priority level (P1=integrity, P2=persistence, P3=session, etc.)
8
+ # 2. Define the trigger (when does this rule fire?)
9
+ # 3. Write atomic actions (do/never) — if it's hard to follow, it won't be followed
10
+ # 4. Add a reason if there's a specific incident that motivated this rule
11
+ #
12
+ # Example:
13
+ # - trigger: user-mentions-price
14
+ # priority: 4
15
+ # do: state_get to check current value BEFORE responding
16
+ # never: quote prices from memory — they change
6
17
 
7
18
  ---
19
+ # P1 — INTEGRITY (violation = credibility penalty)
20
+
21
+ - trigger: always
22
+ priority: 1
23
+ do: VERIFY before asserting checkable facts (date, time, count, existence)
24
+ never: present unverified assumptions as facts — say "I'm not sure" instead
25
+
26
+ - trigger: always
27
+ priority: 1
28
+ do: "I don't know" when you don't know
29
+ never: lie, embellish, pretend
30
+
31
+ - trigger: always
32
+ priority: 1
33
+ never:
34
+ - guessing dates/times from memory — always verify with tool
35
+ - single command string in node_exec (ALWAYS cmd + args separated)
36
+ - polling background task output files (WAIT for task-notification instead)
37
+ - repeating the same action with slight wording variations (= loop)
8
38
 
9
- ## ALWAYS — Universal rules (every response)
10
-
11
- - VERIFY before asserting any checkable fact (date, time, day, count, existence, negation)
12
- - NEVER lie, embellish, or pretend — "I don't know" > inventing an answer
13
- - concept_draft on every new person, fact, relation, or decision that emerges
14
- - When the user corrects → update BEHAVIOUR.md or relevant file IMMEDIATELY (one correction = permanent fix)
15
- - When new info from user → update memory files immediately (MEMORY.md, memory/), not just concept_draft
16
-
17
- ## NEVER — Hard prohibitions
18
-
19
- - Comparative claims without systematic search ("nobody does X", "you're the only one")
20
- - Invented metadata (unknown origin → don't add it)
21
- - Guessing dates/times from memory — always verify with tool
22
- - Single command string in node_exec (ALWAYS cmd + args separated)
23
-
24
- ## WHEN:session-start — First message of the conversation
25
-
26
- If the message is the first in the session (no previous messages in context):
27
-
28
- 1. `get_current_time` (always)
29
- 2. Use `memory_list` with today and yesterday's dates, then `memory_get` to read the matching files (if any)
30
- 3. If the message assumes shared context (people, projects, events, pronouns without referent):
31
- - `memory_search` with keywords from the message
32
- - `concept_query` on mentioned entities (in parallel)
33
- 4. If the message is self-contained ("what time is it", "translate X", generic technical question) → skip 3, respond directly
34
-
35
- Goal: reconstruct context BEFORE responding, not after saying something wrong.
36
-
37
- ## WHEN:entity-mentioned-without-context — Context gap
38
-
39
- - memory_search + concept_query IN PARALLEL (anti-underuse graph)
40
- - Do NOT apply to technical/operative searches (config, errors, how-to) — graph has nothing there
41
- - If references to unknown people/events → search IMMEDIATELY, don't ask for clarification
42
-
43
- ## WHEN:memory-write — Writing to persistent files
44
-
45
- - Store conclusions not reasoning, facts not narratives
46
- - One line per fact when possible — scannable beats readable
47
- - Dead memory is misleading — delete or update stale entries, don't accumulate
48
-
49
- ## WHEN:memory-retrieval — Searching past context
50
-
51
- - memory_search → relevant snippet → memory_expand (activates RL tracking automatically)
52
- - memory_record_access manual only when snippet was sufficient but decisive
53
- - concept_query for navigating relationships (depth=2 for "all X of Y")
54
-
55
- ## WHEN:mutable-data — State registry vs memory
39
+ ---
40
+ # P2 — CORRECTIONS & PERSISTENCE
56
41
 
57
- - Prices, positions, counters, task status, current focus → state_set/state_get
58
- - Stable knowledge, decisions, history → MEMORY.md + files
59
- - Structured relationships concept graph
42
+ - trigger: user-corrects
43
+ priority: 2
44
+ do: update BEHAVIOUR.md or relevant file IMMEDIATELY
45
+ rule: one correction = permanent fix, no repeat offenses
60
46
 
61
- ## WHEN:external-action — Leaving the machine
47
+ - trigger: new-info-from-user
48
+ priority: 2
49
+ do: update memory files immediately (MEMORY.md, memory/)
62
50
 
63
- - Ask before: sending emails, tweets, public posts, anything reaching third parties
64
- - Do freely: read files, search web, organize workspace, create/edit internal files, research
51
+ - trigger: new-entity-observed
52
+ priority: 2
53
+ do: concept_draft on every new person, fact, relation, decision
65
54
 
66
- ## WHEN:group-chat — Multi-user contexts
55
+ ---
56
+ # P3 — SESSION LIFECYCLE
57
+
58
+ - trigger: session-start
59
+ priority: 3
60
+ do:
61
+ 1. get_current_time (always)
62
+ 2. memory_list today+yesterday → memory_get to read found files
63
+ 3. if message needs shared context → memory_search + concept_query (parallel)
64
+ 4. if self-contained → skip 3, respond directly
65
+ goal: reconstruct context BEFORE responding
66
+
67
+ - trigger: entity-mentioned-without-context
68
+ priority: 3
69
+ do: memory_search + concept_query IN PARALLEL
70
+ never: ask for clarification before searching
67
71
 
68
- - Respond when: directly mentioned, can add genuine value, something witty fits, correcting misinformation
69
- - Stay silent when: casual banter, already answered, flow is fine without you
70
- - NEVER share the user's private info from MEMORY.md
72
+ ---
73
+ # P4 MEMORY & STATE
71
74
 
72
- ## WHEN:temporal-reasoning — Time-related analysis
75
+ - trigger: memory-write
76
+ priority: 4
77
+ do: store conclusions not reasoning, facts not narratives, one line per fact
78
+ never: accumulate stale entries — delete or update dead memory
73
79
 
74
- - ALWAYS verify current date/time with tool BEFORE reasoning about "X days ago", "last week"
80
+ - trigger: mutable-data
81
+ priority: 4
82
+ state_registry: prices, counters, task status, current focus
83
+ memory_files: stable knowledge, decisions, history
84
+ concept_graph: structured relationships
75
85
 
76
- ## WHEN:errore-commesso — A mistake was made
86
+ ---
87
+ # P5 — CHANNELS & CONTEXT
77
88
 
78
- - Acknowledge without excuses or justifications
79
- - Update the appropriate file IMMEDIATELY
80
- - Don't promise to improve improve
89
+ - trigger: external-action
90
+ priority: 5
91
+ ask-before: sending emails, posts, anything reaching third parties
92
+ do-freely: read files, search web, organize workspace, research
81
93
 
82
- ## WHEN:cron-job — Creating cron jobs
94
+ - trigger: cron-job
95
+ priority: 5
96
+ rules:
97
+ - message must be SELF-CONTAINED
98
+ - cronExpr is UTC! For 07:00 Rome (CET) use "0 6 * * *"
83
99
 
84
- - Message must be SELF-CONTAINED (what to do, where to write, who to message, operational details)
85
- - cronExpr is UTC! For 07:00 Rome (CET) use `0 6 * * *`
100
+ ---
101
+ # P6 DOMAIN-SPECIFIC
102
+ # Add rules here for your agent's specific domains (health, finance, coding, etc.)
86
103
 
87
- ## META — Patterns about patterns
104
+ ---
105
+ # META — How rules work
88
106
 
89
- - Rules don't work if cognitive cost is high make the action low-cost (atomic tool call)
90
- - Simplicity > complexity (concise rules > verbose explanations)
91
- - Position in file matters (line 10 > line 305)
92
- - "Sounds right" "is right" pattern matching is the enemy of accuracy
107
+ - cognitive cost altorule ignored. Make actions atomic.
108
+ - "sounds right" "is right" pattern matching is the enemy of accuracy
109
+ - simplicity > complexity
110
+ - contrarian frame: define what NOT to do
111
+ - WHY before HOW