fix(apps): stop task.yaml rewrite races + blank-load watchdog + loop-lag monitor

Investigating 'apps blank while a bg task runs': measured serving latency
through a real scheduled bg-task run — 1-3ms on both loopbacks throughout,
so the server is not the bottleneck. Found and fixed the likely renderer-era
culprit plus made the failure observable:

- agent sync (apps:list, polled every 4s) rewrote each bundled agent's
  task.yaml unconditionally — racing the bg runner's own patches mid-run and
  spamming watcher events; now writes only when the definition changed
- AppFrame load watchdog: if the iframe hasn't loaded in 6s, show a visible
  'taking too long' state with Retry instead of a silent blank pane
- main event-loop lag monitor: stalls >300ms are logged so future blank
  reports can be tied to the offending work
This commit is contained in:
Gagan 2026-07-05 00:45:30 +05:30
parent 9173097715
commit 7c9fe7214d
3 changed files with 65 additions and 2 deletions

View file

@ -59,6 +59,9 @@ async function materializeAgent(folder: string, agentFile: string): Promise<stri
if (await pathExists(taskYaml)) {
// Update path (§8.4): overwrite definition fields, preserve the rest.
// Write ONLY when the definition actually changed — this sync runs on
// every apps:list poll, and an unconditional rewrite races the bg
// runner's own task.yaml patches mid-run (and spams watcher events).
try {
const current = parseYaml(await fs.readFile(taskYaml, 'utf-8')) as BackgroundTask;
const next: BackgroundTask = {
@ -69,7 +72,14 @@ async function materializeAgent(folder: string, agentFile: string): Promise<stri
sourceApp: folder,
};
if (!def.triggers) delete next.triggers;
await fs.writeFile(taskYaml, stringifyYaml(next), 'utf-8');
const unchanged =
current.name === next.name &&
current.instructions === next.instructions &&
current.sourceApp === next.sourceApp &&
JSON.stringify(current.triggers ?? null) === JSON.stringify(next.triggers ?? null);
if (!unchanged) {
await fs.writeFile(taskYaml, stringifyYaml(next), 'utf-8');
}
} catch (e) {
console.warn(`[Apps] failed to update agent task ${slug}:`, e);
}

View file

@ -710,6 +710,27 @@ export function getServerStatus(): { running: boolean; error?: string } {
return server ? { running: true } : { running: false, ...(serverError ? { error: serverError } : {}) };
}
let lagMonitor: NodeJS.Timeout | null = null;
/**
* Event-loop lag monitor: the apps server shares the main process with agent
* runs, sync pipelines, etc. If the loop stalls, every open app hangs with it
* log stalls >300ms so "app went blank" reports can be tied to a culprit.
*/
function startLagMonitor(): void {
if (lagMonitor) return;
let last = Date.now();
lagMonitor = setInterval(() => {
const now = Date.now();
const lag = now - last - 500;
if (lag > 300) {
console.warn(`[Apps] main event-loop stalled ~${lag}ms (apps server shares this loop — open apps hang during stalls)`);
}
last = now;
}, 500);
lagMonitor.unref?.();
}
export async function init(): Promise<void> {
if (server) return;
if (startPromise) return startPromise;
@ -717,6 +738,7 @@ export async function init(): Promise<void> {
startPromise = (async () => {
try {
await fsp.mkdir(APPS_DIR, { recursive: true });
startLagMonitor();
await startWatcher();
const expressApp = createApp();
await new Promise<void>((resolve, reject) => {