diff --git a/src/gateway/gateway.ts b/src/gateway/gateway.ts index 89ea5be..f4ca405 100644 --- a/src/gateway/gateway.ts +++ b/src/gateway/gateway.ts @@ -30,7 +30,7 @@ import type { TransportAdapter } from "../transports"; import { hostname } from "node:os"; import { join } from "node:path"; import { injectToolsSection } from "./tools-inject"; -import { injectPersonaSection } from "./persona-inject"; +import { injectPersonaSection, loadPersona } from "./persona-inject"; /** Bot username for command suffix validation (set during gateway init) */ let _botUsername = ""; @@ -328,6 +328,9 @@ export class Gateway { await handleOrAbort(thread, message); }); + // ── Load persona files at startup (cached for process lifetime) ─── + loadPersona(); + // ── Handle inline keyboard callbacks ─── this.chat.onAction(MODEL_ACTION_ID, async (event: any) => { await handleModelAction({ value: event.value, thread: event.thread }); diff --git a/src/gateway/persona-inject.ts b/src/gateway/persona-inject.ts index 7d5ba89..7625593 100644 --- a/src/gateway/persona-inject.ts +++ b/src/gateway/persona-inject.ts @@ -5,9 +5,9 @@ * prepends them as a structured section so the agent has identity and * user context on every turn. * - * No caching — these files are expected to be updated by the agent - * during conversations (especially user.md). Files are small (<2KB), - * so readFileSync on each turn is negligible. + * Cached on first load (gateway startup). Call reloadPersona() after + * the agent edits user.md/soul.md to pick up changes within the same + * process lifetime. Otherwise changes take effect on next restart. */ import { readFileSync } from "node:fs"; @@ -15,6 +15,8 @@ import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { ROUNDHOUSE_DIR } from "../config"; +let cachedPersona: string | null = null; + function loadFile(filename: string): string { const userPath = join(ROUNDHOUSE_DIR, filename); const bundledPath = join(dirname(fileURLToPath(import.meta.url)), filename); @@ -30,20 +32,40 @@ function loadFile(filename: string): string { } } -/** - * Prepend a section to the prompt text. - * Only injects if soul.md or user.md have content. - */ -export function injectPersonaSection(text: string): string { +function buildPersona(): string { const soul = loadFile("soul.md").trim(); const user = loadFile("user.md").trim(); - if (!soul && !user) return text; + if (!soul && !user) return ""; const parts: string[] = []; if (soul) parts.push(soul); if (user) parts.push(user); - const persona = parts.join("\n\n---\n\n"); + return parts.join("\n\n---\n\n"); +} + +/** + * Load persona files and cache the result. + * Call at gateway startup to eagerly load. + */ +export function loadPersona(): void { + cachedPersona = buildPersona(); +} + +/** + * Reload persona from disk. Call after agent edits user.md/soul.md + * (e.g. from an IPC handler or post-tool-execution hook). + */ +export function reloadPersona(): void { + cachedPersona = buildPersona(); +} - return `\n${persona}\n\n\n${text}`; +/** + * Prepend a section to the prompt text. + * Only injects if soul.md or user.md have content. + */ +export function injectPersonaSection(text: string): string { + if (cachedPersona === null) loadPersona(); + if (!cachedPersona) return text; + return `\n${cachedPersona}\n\n\n${text}`; }