fix(config): persist runtime patches to active overlay config path

This commit is contained in:
William Valentin
2026-02-18 19:43:55 -08:00
parent 0a664ddb21
commit 7e480f11fc
5 changed files with 38 additions and 5 deletions
+14
View File
@@ -5669,6 +5669,20 @@
"docs/plans/state.json" "docs/plans/state.json"
], ],
"test_status": "pnpm typecheck passing" "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": { "overall_progress": {
+3 -2
View File
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import { dirname } from 'path'; import { dirname } from 'path';
import { createInterface } from 'readline/promises'; import { createInterface } from 'readline/promises';
import { parse } from 'yaml'; import { parse } from 'yaml';
import { getConfigPath } from './shared.js'; import { getConfigPath, resolveEffectiveConfigPath } from './shared.js';
import { createPrompter } from './setup/prompts.js'; import { createPrompter } from './setup/prompts.js';
import { ConfigBuilder } from './setup/config.js'; import { ConfigBuilder } from './setup/config.js';
import { runFirstRunWizard, runMenu } from './setup/orchestrator.js'; import { runFirstRunWizard, runMenu } from './setup/orchestrator.js';
@@ -47,7 +47,8 @@ export async function runSetup(configPath: string): Promise<void> {
const { startDaemon } = await import('../daemon/index.js'); const { startDaemon } = await import('../daemon/index.js');
const { loadConfig } = await import('../config/index.js'); const { loadConfig } = await import('../config/index.js');
const config = loadConfig(configPath); const config = loadConfig(configPath);
const daemon = await startDaemon(config, { configPath }); const persistConfigPath = resolveEffectiveConfigPath(configPath);
const daemon = await startDaemon(config, { configPath, persistConfigPath });
await new Promise<void>(resolve => daemon.lifecycle.onShutdown(async () => resolve())); await new Promise<void>(resolve => daemon.lifecycle.onShutdown(async () => resolve()));
return; return;
} }
+13
View File
@@ -58,6 +58,19 @@ export function resolveOverlayPath(basePath: string): string | undefined {
return join(configDir, `${env}.yaml`); 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 }. */ /** Load config without throwing. Returns { config } or { error }. */
export function loadConfigSafe(configPath?: string): { config?: Config; error?: string } { export function loadConfigSafe(configPath?: string): { config?: Config; error?: string } {
const path = configPath ?? getConfigPath(); const path = configPath ?? getConfigPath();
+6 -2
View File
@@ -1,5 +1,5 @@
import type { Command } from 'commander'; import type { Command } from 'commander';
import { loadConfigSafe, getConfigPath } from './shared.js'; import { loadConfigSafe, getConfigPath, resolveEffectiveConfigPath } from './shared.js';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
export function registerStartCommand(program: Command): void { export function registerStartCommand(program: Command): void {
@@ -9,6 +9,7 @@ export function registerStartCommand(program: Command): void {
.option('-c, --config <path>', 'Config file path') .option('-c, --config <path>', 'Config file path')
.action(async (opts: { config?: string }) => { .action(async (opts: { config?: string }) => {
const configPath = opts.config ?? getConfigPath(); const configPath = opts.config ?? getConfigPath();
const persistConfigPath = resolveEffectiveConfigPath(configPath);
if (!existsSync(configPath)) { if (!existsSync(configPath)) {
// Offer setup wizard // Offer setup wizard
@@ -35,6 +36,9 @@ export function registerStartCommand(program: Command): void {
console.log('Flynn starting...'); console.log('Flynn starting...');
console.log(`Loading config from: ${configPath}`); console.log(`Loading config from: ${configPath}`);
if (persistConfigPath !== configPath) {
console.log(`Runtime config persistence target: ${persistConfigPath}`);
}
const { config, error } = loadConfigSafe(configPath); const { config, error } = loadConfigSafe(configPath);
if (!config) { if (!config) {
@@ -44,7 +48,7 @@ export function registerStartCommand(program: Command): void {
// Dynamic import to avoid loading daemon code for other commands // Dynamic import to avoid loading daemon code for other commands
const { startDaemon } = await import('../daemon/index.js'); const { startDaemon } = await import('../daemon/index.js');
const daemon = await startDaemon(config, { configPath }); const daemon = await startDaemon(config, { configPath, persistConfigPath });
if (config.telegram) { if (config.telegram) {
console.log(`Allowed Telegram chat IDs: ${config.telegram.allowed_chat_ids.join(', ')}`); console.log(`Allowed Telegram chat IDs: ${config.telegram.allowed_chat_ids.join(', ')}`);
+2 -1
View File
@@ -108,6 +108,7 @@ export interface DaemonContext {
export interface StartDaemonOptions { export interface StartDaemonOptions {
configPath?: string; configPath?: string;
persistConfigPath?: string;
} }
export async function startDaemon(config: Config, options?: StartDaemonOptions): Promise<DaemonContext> { export async function startDaemon(config: Config, options?: StartDaemonOptions): Promise<DaemonContext> {
@@ -206,7 +207,7 @@ export async function startDaemon(config: Config, options?: StartDaemonOptions):
let channelAgents: ReturnType<typeof createMessageRouter>['agents'] | null = null; let channelAgents: ReturnType<typeof createMessageRouter>['agents'] | null = null;
const gateway = createGateway({ 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, channelRegistry, pairingManager, lifecycle, memoryStore,
getChannelAgents: () => channelAgents, commandRegistry, intentRegistry, routingPolicy, hookEngine, getChannelAgents: () => channelAgents, commandRegistry, intentRegistry, routingPolicy, hookEngine,
}); });