diff --git a/docs/plans/state.json b/docs/plans/state.json index c0ff662..91c9b10 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -5669,6 +5669,20 @@ "docs/plans/state.json" ], "test_status": "pnpm typecheck passing" + }, + "overlay-aware-runtime-config-persistence": { + "status": "completed", + "date": "2026-02-19", + "updated": "2026-02-19", + "summary": "Made runtime config persistence overlay-aware: when `FLYNN_ENV` overlay file is active, daemon/gateway config.patch now persists to the effective overlay config path instead of always writing base config.yaml.", + "files_modified": [ + "src/cli/shared.ts", + "src/cli/start.ts", + "src/cli/setup.ts", + "src/daemon/index.ts", + "docs/plans/state.json" + ], + "test_status": "pnpm typecheck passing" } }, "overall_progress": { diff --git a/src/cli/setup.ts b/src/cli/setup.ts index a0c9fd5..862744f 100644 --- a/src/cli/setup.ts +++ b/src/cli/setup.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs'; import { dirname } from 'path'; import { createInterface } from 'readline/promises'; import { parse } from 'yaml'; -import { getConfigPath } from './shared.js'; +import { getConfigPath, resolveEffectiveConfigPath } from './shared.js'; import { createPrompter } from './setup/prompts.js'; import { ConfigBuilder } from './setup/config.js'; import { runFirstRunWizard, runMenu } from './setup/orchestrator.js'; @@ -47,7 +47,8 @@ export async function runSetup(configPath: string): Promise { const { startDaemon } = await import('../daemon/index.js'); const { loadConfig } = await import('../config/index.js'); const config = loadConfig(configPath); - const daemon = await startDaemon(config, { configPath }); + const persistConfigPath = resolveEffectiveConfigPath(configPath); + const daemon = await startDaemon(config, { configPath, persistConfigPath }); await new Promise(resolve => daemon.lifecycle.onShutdown(async () => resolve())); return; } diff --git a/src/cli/shared.ts b/src/cli/shared.ts index 95af5b3..b0dc384 100644 --- a/src/cli/shared.ts +++ b/src/cli/shared.ts @@ -58,6 +58,19 @@ export function resolveOverlayPath(basePath: string): string | undefined { return join(configDir, `${env}.yaml`); } +/** + * Resolve the effective config source path for runtime persistence. + * When FLYNN_ENV is set and overlay file exists, persist to the overlay file. + * Otherwise persist to the base config path. + */ +export function resolveEffectiveConfigPath(basePath: string): string { + const overlayPath = resolveOverlayPath(basePath); + if (overlayPath && existsSync(overlayPath)) { + return overlayPath; + } + return basePath; +} + /** Load config without throwing. Returns { config } or { error }. */ export function loadConfigSafe(configPath?: string): { config?: Config; error?: string } { const path = configPath ?? getConfigPath(); diff --git a/src/cli/start.ts b/src/cli/start.ts index a26cf16..aa6e7a0 100644 --- a/src/cli/start.ts +++ b/src/cli/start.ts @@ -1,5 +1,5 @@ import type { Command } from 'commander'; -import { loadConfigSafe, getConfigPath } from './shared.js'; +import { loadConfigSafe, getConfigPath, resolveEffectiveConfigPath } from './shared.js'; import { existsSync } from 'fs'; export function registerStartCommand(program: Command): void { @@ -9,6 +9,7 @@ export function registerStartCommand(program: Command): void { .option('-c, --config ', 'Config file path') .action(async (opts: { config?: string }) => { const configPath = opts.config ?? getConfigPath(); + const persistConfigPath = resolveEffectiveConfigPath(configPath); if (!existsSync(configPath)) { // Offer setup wizard @@ -35,6 +36,9 @@ export function registerStartCommand(program: Command): void { console.log('Flynn starting...'); console.log(`Loading config from: ${configPath}`); + if (persistConfigPath !== configPath) { + console.log(`Runtime config persistence target: ${persistConfigPath}`); + } const { config, error } = loadConfigSafe(configPath); if (!config) { @@ -44,7 +48,7 @@ export function registerStartCommand(program: Command): void { // Dynamic import to avoid loading daemon code for other commands const { startDaemon } = await import('../daemon/index.js'); - const daemon = await startDaemon(config, { configPath }); + const daemon = await startDaemon(config, { configPath, persistConfigPath }); if (config.telegram) { console.log(`Allowed Telegram chat IDs: ${config.telegram.allowed_chat_ids.join(', ')}`); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 53695f1..7be7990 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -108,6 +108,7 @@ export interface DaemonContext { export interface StartDaemonOptions { configPath?: string; + persistConfigPath?: string; } export async function startDaemon(config: Config, options?: StartDaemonOptions): Promise { @@ -206,7 +207,7 @@ export async function startDaemon(config: Config, options?: StartDaemonOptions): let channelAgents: ReturnType['agents'] | null = null; const gateway = createGateway({ - config, configPath: options?.configPath, sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor, + config, configPath: options?.persistConfigPath ?? options?.configPath, sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor, channelRegistry, pairingManager, lifecycle, memoryStore, getChannelAgents: () => channelAgents, commandRegistry, intentRegistry, routingPolicy, hookEngine, });