Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/gateway/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "";
Expand Down Expand Up @@ -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 });
Expand Down
44 changes: 33 additions & 11 deletions src/gateway/persona-inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
* 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";
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);
Expand All @@ -30,20 +32,40 @@ function loadFile(filename: string): string {
}
}

/**
* Prepend a <persona> 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 `<persona>\n${persona}\n</persona>\n\n${text}`;
/**
* Prepend a <persona> 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;
Comment on lines +68 to +69
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wire persona reload when persona files change

Caching soul.md/user.md for process lifetime introduces a behavior regression: updates to those files are no longer reflected in subsequent turns unless reloadPersona() is called, but there is no call site for reloadPersona() (gateway startup only calls loadPersona() in src/gateway/gateway.ts). In workflows where the agent or user edits these files during runtime, prompts keep using stale persona content until a restart, which breaks dynamic persona updates that previously worked turn-by-turn.

Useful? React with 👍 / 👎.

return `<persona>\n${cachedPersona}\n</persona>\n\n${text}`;
}
Loading