refactor: extract memfs resolution with server-mode awareness (#560)
This commit is contained in:
58
src/config/memfs.test.ts
Normal file
58
src/config/memfs.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveSessionMemfs } from './memfs.js';
|
||||
|
||||
describe('resolveSessionMemfs', () => {
|
||||
it('uses explicit agent config first', () => {
|
||||
const result = resolveSessionMemfs({
|
||||
configuredMemfs: true,
|
||||
envMemfs: 'false',
|
||||
serverMode: 'api',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ value: true, source: 'config' });
|
||||
});
|
||||
|
||||
it('uses LETTABOT_MEMFS env override when config is unset', () => {
|
||||
const result = resolveSessionMemfs({
|
||||
envMemfs: 'false',
|
||||
serverMode: 'api',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ value: false, source: 'env' });
|
||||
});
|
||||
|
||||
it('defaults to memfs false in docker/selfhosted mode when unset', () => {
|
||||
const result = resolveSessionMemfs({
|
||||
serverMode: 'selfhosted',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ value: false, source: 'default-docker' });
|
||||
});
|
||||
|
||||
it('leaves memfs unchanged in api/cloud mode when unset', () => {
|
||||
const result = resolveSessionMemfs({
|
||||
serverMode: 'cloud',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ value: undefined, source: 'unset' });
|
||||
});
|
||||
|
||||
it('ignores invalid LETTABOT_MEMFS values', () => {
|
||||
const result = resolveSessionMemfs({
|
||||
envMemfs: 'yes',
|
||||
serverMode: 'api',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ value: undefined, source: 'unset' });
|
||||
});
|
||||
|
||||
it('treats null configured memfs as unset and applies docker default', () => {
|
||||
const result = resolveSessionMemfs({
|
||||
configuredMemfs: null as unknown as boolean,
|
||||
serverMode: 'selfhosted',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ value: false, source: 'default-docker' });
|
||||
});
|
||||
});
|
||||
48
src/config/memfs.ts
Normal file
48
src/config/memfs.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { isDockerServerMode, type ServerMode } from './types.js';
|
||||
|
||||
export type ResolvedMemfsSource = 'config' | 'env' | 'default-docker' | 'unset';
|
||||
|
||||
export interface ResolveSessionMemfsInput {
|
||||
configuredMemfs?: boolean;
|
||||
envMemfs?: string;
|
||||
serverMode?: ServerMode;
|
||||
}
|
||||
|
||||
export interface ResolveSessionMemfsResult {
|
||||
value: boolean | undefined;
|
||||
source: ResolvedMemfsSource;
|
||||
}
|
||||
|
||||
function parseBooleanEnv(value?: string): boolean | undefined {
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the memfs value forwarded to SDK session options.
|
||||
*
|
||||
* Precedence:
|
||||
* 1) Per-agent config (`features.memfs`)
|
||||
* 2) `LETTABOT_MEMFS` env var (`true`/`false`)
|
||||
* 3) Default `false` in docker/self-hosted mode (safety)
|
||||
* 4) `undefined` in API mode (leave agent memfs unchanged)
|
||||
*/
|
||||
export function resolveSessionMemfs(input: ResolveSessionMemfsInput): ResolveSessionMemfsResult {
|
||||
// Runtime config parsing can surface non-boolean values (e.g. YAML `memfs:` -> null).
|
||||
// Only treat explicit booleans as configured; everything else falls through.
|
||||
if (typeof input.configuredMemfs === 'boolean') {
|
||||
return { value: input.configuredMemfs, source: 'config' };
|
||||
}
|
||||
|
||||
const envMemfs = parseBooleanEnv(input.envMemfs);
|
||||
if (envMemfs !== undefined) {
|
||||
return { value: envMemfs, source: 'env' };
|
||||
}
|
||||
|
||||
if (isDockerServerMode(input.serverMode)) {
|
||||
return { value: false, source: 'default-docker' };
|
||||
}
|
||||
|
||||
return { value: undefined, source: 'unset' };
|
||||
}
|
||||
@@ -20,6 +20,23 @@ import { createLogger } from '../logger.js';
|
||||
|
||||
const log = createLogger('Session');
|
||||
|
||||
function formatMemfsStartupOption(memfs: boolean | undefined): string {
|
||||
if (memfs === true) return 'enabled (--memfs)';
|
||||
if (memfs === false) return 'disabled (--no-memfs)';
|
||||
return 'unchanged (omitted)';
|
||||
}
|
||||
|
||||
function formatSleeptimeStartupOption(
|
||||
sleeptime: BotConfig['sleeptime'],
|
||||
): string {
|
||||
if (!sleeptime) return 'none';
|
||||
const parts: string[] = [];
|
||||
if (sleeptime.trigger) parts.push(`trigger=${sleeptime.trigger}`);
|
||||
if (sleeptime.behavior) parts.push(`behavior=${sleeptime.behavior}`);
|
||||
if (sleeptime.stepCount !== undefined) parts.push(`stepCount=${sleeptime.stepCount}`);
|
||||
return parts.length > 0 ? parts.join(', ') : 'configured';
|
||||
}
|
||||
|
||||
function toConcreteConversationId(value: string | null | undefined): string | null {
|
||||
if (!value) return null;
|
||||
const trimmed = value.trim();
|
||||
@@ -313,6 +330,11 @@ export class SessionManager {
|
||||
}
|
||||
|
||||
// Initialize eagerly so the subprocess is ready before the first send()
|
||||
log.info(
|
||||
`Session startup options (key=${key}): ` +
|
||||
`memfs=${formatMemfsStartupOption(this.config.memfs)}, ` +
|
||||
`sleeptime=${formatSleeptimeStartupOption(this.config.sleeptime)}`,
|
||||
);
|
||||
log.info(`Initializing session subprocess (key=${key})...`);
|
||||
try {
|
||||
await this.withSessionTimeout(session.initialize(), `Session initialize (key=${key})`);
|
||||
|
||||
25
src/main.ts
25
src/main.ts
@@ -24,6 +24,7 @@ import {
|
||||
serverModeLabel,
|
||||
wasLoadedFromFleetConfig,
|
||||
} from './config/index.js';
|
||||
import { resolveSessionMemfs } from './config/memfs.js';
|
||||
import { getCronDataDir, getDataDir, getWorkingDir, hasRailwayVolume, resolveWorkingDirPath } from './utils/paths.js';
|
||||
import { parseCsvList, parseNonNegativeNumber } from './utils/parse.js';
|
||||
import { createLogger, setLogLevel } from './logger.js';
|
||||
@@ -299,21 +300,23 @@ async function main() {
|
||||
for (const agentConfig of agents) {
|
||||
log.info(`Configuring agent: ${agentConfig.name}`);
|
||||
|
||||
// Resolve memfs: YAML config takes precedence, then env var, then default false.
|
||||
// Default false prevents the SDK from auto-enabling memfs, which crashes on
|
||||
// self-hosted Letta servers that don't have the git endpoint.
|
||||
const resolvedMemfs = agentConfig.features?.memfs ?? (process.env.LETTABOT_MEMFS === 'true' ? true : false);
|
||||
const resolvedMemfsResult = resolveSessionMemfs({
|
||||
configuredMemfs: agentConfig.features?.memfs,
|
||||
envMemfs: process.env.LETTABOT_MEMFS,
|
||||
serverMode: yamlConfig.server.mode,
|
||||
});
|
||||
const resolvedMemfs = resolvedMemfsResult.value;
|
||||
const configuredSleeptime = agentConfig.features?.sleeptime;
|
||||
// Treat missing trigger as active (conservative): only `trigger: 'off'` explicitly disables.
|
||||
const sleeptimeRequiresMemfs = !!configuredSleeptime && configuredSleeptime.trigger !== 'off';
|
||||
const effectiveSleeptime = !resolvedMemfs && sleeptimeRequiresMemfs
|
||||
const effectiveSleeptime = resolvedMemfs === false && sleeptimeRequiresMemfs
|
||||
? undefined
|
||||
: configuredSleeptime;
|
||||
|
||||
if (!resolvedMemfs && sleeptimeRequiresMemfs) {
|
||||
if (resolvedMemfs === false && sleeptimeRequiresMemfs) {
|
||||
log.warn(
|
||||
`Agent ${agentConfig.name}: sleeptime is configured but memfs is disabled; ` +
|
||||
`sleeptime will be ignored. Enable features.memfs (or LETTABOT_MEMFS=true) to use sleeptime.`
|
||||
`sleeptime will be ignored. Enable features.memfs (or set LETTABOT_MEMFS=true) to use sleeptime.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -359,8 +362,14 @@ async function main() {
|
||||
|
||||
// Log memfs config (from either YAML or env var)
|
||||
if (resolvedMemfs !== undefined) {
|
||||
const source = agentConfig.features?.memfs !== undefined ? '' : ' (from LETTABOT_MEMFS env)';
|
||||
const source = resolvedMemfsResult.source === 'config'
|
||||
? ''
|
||||
: resolvedMemfsResult.source === 'env'
|
||||
? ' (from LETTABOT_MEMFS env)'
|
||||
: ' (default for docker/selfhosted mode)';
|
||||
log.info(`Agent ${agentConfig.name}: memfs ${resolvedMemfs ? 'enabled' : 'disabled'}${source}`);
|
||||
} else {
|
||||
log.info(`Agent ${agentConfig.name}: memfs unchanged (not explicitly configured)`);
|
||||
}
|
||||
|
||||
// Apply explicit agent ID from config (before store verification)
|
||||
|
||||
Reference in New Issue
Block a user