fix(agent): await child completion in /agent/v1/exec route

Fastify-handler returnde direct na het attachen van event handlers.
Fastify finaliseerde dan de reply waardoor `req.raw.on('close')` direct
firede en `child.kill()` aanriep voordat het kind iets kon produceren.
Symptoom: SSE bevatte alleen `event:exit code:null` zonder stdout/stderr,
audit-log toonde `exit_code:null duration_ms:0`, dashboard-modules
toonden "No containers running" / "No data" terwijl handmatige command
prima werkte.

Wrap de event-handlers in een Promise zodat de async route-handler wacht
op child close/error voordat ie returnt. Verplaats client-disconnect
detectie van `req.raw.on('close')` naar `reply.raw.on('close')` — die
fired bij echte connectie-sluiting, niet bij request body parse.

Bevestigd: `docker_ps` retourneert nu volledige container-lijst, dashboard
/docker pagina rendert alle 6 containers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-13 22:23:30 +02:00
parent f7821c05be
commit 6581a9ef33

View file

@ -108,20 +108,33 @@ export async function execRoutes(app: FastifyInstance): Promise<void> {
sendEvent('stderr', chunk.toString()); sendEvent('stderr', chunk.toString());
}); });
// Houd de route-handler open totdat het kind klaar is. Zonder dit return-t
// de async functie meteen, finaliseert Fastify de reply, en triggert dat
// `req.raw.on('close')` → `child.kill()` voordat het kind iets kon doen.
await new Promise<void>((resolve) => {
let settled = false
const finish = () => {
if (settled) return
settled = true
resolve()
}
child.on('close', (code) => { child.on('close', (code) => {
auditLog(command_key, args, code, Date.now() - startedAt); auditLog(command_key, args, code, Date.now() - startedAt)
reply.raw.write(`event: exit\ndata: ${JSON.stringify({ code })}\n\n`); reply.raw.write(`event: exit\ndata: ${JSON.stringify({ code })}\n\n`)
reply.raw.end(); reply.raw.end()
}); finish()
})
child.on('error', (err) => { child.on('error', (err) => {
auditLog(command_key, args, null, Date.now() - startedAt); auditLog(command_key, args, null, Date.now() - startedAt)
reply.raw.write(`event: error\ndata: ${JSON.stringify({ message: err.message })}\n\n`); reply.raw.write(`event: error\ndata: ${JSON.stringify({ message: err.message })}\n\n`)
reply.raw.end(); reply.raw.end()
}); finish()
})
req.raw.on('close', () => { // Detect client disconnect via response stream (niet request stream —
child.kill(); // die fired al direct na request body parse).
}); reply.raw.on('close', () => {
if (!settled) child.kill()
})
})
}); });
} }