From ba19e1fcda0b360f0ca4e02d24f8fad47f249b48 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 8 Jan 2026 16:55:45 -0500 Subject: [PATCH] fix(api): add optimistic locking for thinking config - W4: return lastModified on GET /api/thinking - W4: check mtime on PUT, return 409 on conflict --- src/web-server/routes/misc-routes.ts | 47 +++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/web-server/routes/misc-routes.ts b/src/web-server/routes/misc-routes.ts index a8deae6d..12228ac0 100644 --- a/src/web-server/routes/misc-routes.ts +++ b/src/web-server/routes/misc-routes.ts @@ -12,6 +12,7 @@ import { saveUnifiedConfig, getGlobalEnvConfig, getThinkingConfig, + getConfigYamlPath, } from '../../config/unified-config-loader'; import type { ThinkingConfig } from '../../config/unified-config-types'; import { @@ -233,12 +234,20 @@ router.put('/global-env', (req: Request, res: Response): void => { /** * GET /api/thinking - Get thinking budget configuration - * Returns the thinking section from config.yaml + * Returns the thinking section from config.yaml with mtime for optimistic locking */ router.get('/thinking', (_req: Request, res: Response): void => { try { const config = getThinkingConfig(); - res.json({ config }); + // W4: Include mtime for optimistic locking + let lastModified: number | undefined; + try { + const stats = fs.statSync(getConfigYamlPath()); + lastModified = stats.mtimeMs; + } catch { + // File may not exist yet + } + res.json({ config, lastModified }); } catch (error) { res.status(500).json({ error: (error as Error).message }); } @@ -247,10 +256,30 @@ router.get('/thinking', (_req: Request, res: Response): void => { /** * PUT /api/thinking - Update thinking budget configuration * Updates the thinking section in config.yaml + * Supports optimistic locking via lastModified field */ router.put('/thinking', (req: Request, res: Response): void => { try { - const updates = req.body as Partial; + const { lastModified, ...updates } = req.body as Partial & { + lastModified?: number; + }; + + // W4: Optimistic locking - check if file was modified since last read + if (lastModified !== undefined) { + try { + const stats = fs.statSync(getConfigYamlPath()); + if (stats.mtimeMs > lastModified) { + res.status(409).json({ + error: 'Config was modified by another process. Please refresh and try again.', + currentMtime: stats.mtimeMs, + }); + return; + } + } catch { + // File may not exist yet, allow creation + } + } + const config = loadOrCreateUnifiedConfig(); // Validate mode if provided @@ -353,7 +382,17 @@ router.put('/thinking', (req: Request, res: Response): void => { }; saveUnifiedConfig(config); - res.json({ success: true, config: config.thinking }); + + // W4: Return new mtime for subsequent requests + let newMtime: number | undefined; + try { + const stats = fs.statSync(getConfigYamlPath()); + newMtime = stats.mtimeMs; + } catch { + // Ignore + } + + res.json({ success: true, config: config.thinking, lastModified: newMtime }); } catch (error) { res.status(500).json({ error: (error as Error).message }); }