diff --git a/apps/x/packages/core/src/apps/server.ts b/apps/x/packages/core/src/apps/server.ts index 5861f7d6..58565e7b 100644 --- a/apps/x/packages/core/src/apps/server.ts +++ b/apps/x/packages/core/src/apps/server.ts @@ -683,6 +683,38 @@ function slugFromAbsolutePath(absolutePath: string): { slug: string; rel: string return { slug, rel: segments.slice(1).join('/') }; } +// Config-change → one-shot agent run. An app writes data/config.json when the +// user changes its settings (e.g. picks the repo to track); its bundled agents +// are what turn that config into fresh data. Without this kick the user would +// stare at an empty app until the next cron tick. Generic: any app, any agent +// the app owns (app----*). Dynamic import keeps apps/server decoupled +// from the bg-task runner at module-load time. +const agentKickTimers = new Map(); +function scheduleAgentKick(slug: string): void { + const existing = agentKickTimers.get(slug); + if (existing) clearTimeout(existing); + agentKickTimers.set(slug, setTimeout(() => { + agentKickTimers.delete(slug); + void (async () => { + try { + const tasksDir = path.join(path.dirname(APPS_DIR), 'bg-tasks'); + const entries = await fsp.readdir(tasksDir).catch(() => [] as string[]); + const owned = entries.filter((e) => e.startsWith(`app--${slug}--`)); + if (!owned.length) return; + const { runBackgroundTask } = await import('../background-tasks/runner.js'); + for (const taskSlug of owned) { + console.log(`[Apps] ${slug}/data/config.json changed — running ${taskSlug}`); + void runBackgroundTask(taskSlug, 'manual').catch((e: unknown) => { + console.warn(`[Apps] config-change agent run failed for ${taskSlug}:`, e); + }); + } + } catch (e) { + console.warn(`[Apps] config-change agent kick failed for ${slug}:`, e); + } + })(); + }, 400)); +} + async function startWatcher(): Promise { if (watcher) return; const w = chokidar.watch(APPS_DIR, { @@ -695,6 +727,9 @@ async function startWatcher(): Promise { if (!hit || hit.rel.endsWith('.tmp') || /\.tmp-[0-9a-f]+$/.test(hit.rel)) return; const area: 'dist' | 'data' = hit.rel === 'data' || hit.rel.startsWith('data/') ? 'data' : 'dist'; scheduleChangeBroadcast(hit.slug, area, hit.rel); + if (hit.rel === 'data/config.json' && (eventName === 'add' || eventName === 'change')) { + scheduleAgentKick(hit.slug); + } }); w.on('error', (error: unknown) => { console.error('[Apps] watcher error:', error); @@ -795,6 +830,8 @@ export async function shutdown(): Promise { for (const timer of reloadTimers.values()) clearTimeout(timer); reloadTimers.clear(); + for (const timer of agentKickTimers.values()) clearTimeout(timer); + agentKickTimers.clear(); for (const clients of eventClients.values()) { for (const res of clients) {