Add text features: trusted triggers, context cache, response policy, MCP config tools

This commit is contained in:
Fisto
2026-09-11 15:31:23 +02:00
parent 23a1a8e593
commit 82f78bb2b4
20 changed files with 841 additions and 88 deletions
+99
View File
@@ -0,0 +1,99 @@
/*
* Veskslop daemon — per-channel message cache in a tmp folder with a size limit.
*
* The cache backs conversation context. It keeps the newest messages per
* channel and evicts the least recently written channel files when the folder
* exceeds the size limit.
*/
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { compareSnowflakes, type CachedMessage } from "./communication.js";
export const CACHE_DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
const MAX_MESSAGES_PER_CHANNEL = 200;
interface ChannelCacheFile {
messages: CachedMessage[];
}
export class MessageCache {
private readonly dir: string;
private readonly maxBytes: number;
constructor(
dir = process.env.VESKSLOP_CACHE_DIR ?? join(tmpdir(), "veskslop-cache"),
maxBytes = Number(process.env.VESKSLOP_CACHE_MAX_BYTES ?? CACHE_DEFAULT_MAX_BYTES)
) {
this.dir = dir;
this.maxBytes = maxBytes;
mkdirSync(this.dir, { recursive: true });
}
get(channelId: string): CachedMessage[] {
const file = this.file(channelId);
if (!existsSync(file)) return [];
try {
return (JSON.parse(readFileSync(file, "utf8")) as ChannelCacheFile).messages;
} catch {
return [];
}
}
newestId(channelId: string): string | undefined {
return this.get(channelId).at(-1)?.id;
}
/** Merge messages by ID, keep the newest, write, then evict old files. */
merge(channelId: string, incoming: readonly CachedMessage[]): CachedMessage[] {
const byId = new Map<string, CachedMessage>();
for (const message of [...this.get(channelId), ...incoming]) byId.set(message.id, message);
const merged = [...byId.values()]
.sort((a, b) => compareSnowflakes(a.id, b.id))
.slice(-MAX_MESSAGES_PER_CHANNEL);
this.write(channelId, merged);
this.evict();
return merged;
}
private file(channelId: string): string {
return join(this.dir, `${channelId}.json`);
}
private write(channelId: string, messages: CachedMessage[]) {
try {
writeFileSync(this.file(channelId), JSON.stringify({ messages } satisfies ChannelCacheFile));
} catch (error) {
console.error("[cache] write failed:", error);
}
}
private evict() {
try {
const files = readdirSync(this.dir)
.filter(name => name.endsWith(".json"))
.map(name => {
const path = join(this.dir, name);
const stats = statSync(path);
return { path, size: stats.size, mtime: stats.mtimeMs };
});
let total = files.reduce((sum, file) => sum + file.size, 0);
if (total <= this.maxBytes) return;
files.sort((a, b) => a.mtime - b.mtime);
for (const file of files) {
if (total <= this.maxBytes) break;
rmSync(file.path, { force: true });
total -= file.size;
}
} catch (error) {
console.error("[cache] eviction failed:", error);
}
}
}