Flag hung seats and memory pressure

A turn that has run 15 minutes while producing under 50 tokens a minute is a
stalled API stream: flagged on the page with an Esc button, and notified once.
The container's memory use is shown in the header and notified near the limit.

Claude-Session: https://claude.ai/code/session_01QhLkBGEip8wQdaZ54z4AqJ
This commit is contained in:
Asger Geel Weirsøe
2026-10-20 19:45:34 +02:00
parent 150abd9afe
commit 94b4576a96
4 changed files with 95 additions and 6 deletions
+19 -4
View File
@@ -20,13 +20,16 @@ const ago = (iso) => {
};
const seatTag = (s) => {
if (s.stalled) return $('span', { class: 'wt-tag wt-tag--bad' }, `hænger (${s.stalled} min)`);
if (s.waiting) return $('span', { class: 'wt-tag wt-tag--warn' }, 'venter på svar');
if (s.status && s.status !== 'running') return $('span', { class: 'wt-tag wt-tag--bad' }, s.status);
if (s.activity === 'running' || s.activity === 'working') return $('span', { class: 'wt-tag wt-tag--accent' }, 'arbejder');
return $('span', { class: 'wt-tag' }, 'ledig');
};
const QSTATE = { 'in-progress': 'wt-tag--accent', blocked: '', parked: '', done: 'wt-tag--good', failed: 'wt-tag--bad' };
const gb = (bytes) => (bytes / 2 ** 30).toFixed(1);
const QSTATE ={ 'in-progress': 'wt-tag--accent', blocked: '', parked: '', done: 'wt-tag--good', failed: 'wt-tag--bad' };
let snap = null;
let timer = null;
@@ -67,8 +70,13 @@ function replyBox(placeholder, label, onSend) {
// What a seat is asking, with the controls to answer it.
function askBlock(s, after) {
const a = s.ask;
if (!a) return null;
const done = (ok) => { if (after) after(); setTimeout(load, 1500); return ok; };
if (!a && s.stalled) {
return $('div', { class: 'wt-stack' },
$('span', { class: 'wt-muted' }, `Samme tur i ${s.stalled} min uden at producere noget. Esc afbryder, bagefter kan du bede den fortsætte.`),
$('button', { class: 'wt-btn', onclick: async () => done(await post('api/send', { seat: s.name, key: 'esc' })) }, 'Esc (afbryd turen)'));
}
if (!a) return null;
if (a.kind === 'menu') {
return $('div', { class: 'wt-stack' },
a.title ? $('span', {}, a.title) : null,
@@ -85,7 +93,14 @@ function askBlock(s, after) {
function overview() {
const view = [];
const waiting = snap.rigs.flatMap((r) => r.seats.filter((s) => s.waiting));
const mem = snap.memory;
if (mem && mem.fraction >= 0.9) {
view.push($('section', { class: 'wt-card wt-card--raised wt-stack' },
$('div', { class: 'wt-row wt-row--between' }, $('h2', { class: 'wt-card-title' }, 'Hukommelse'),
$('span', { class: 'wt-tag wt-tag--bad' }, `${Math.round(mem.fraction * 100)} %`)),
$('span', {}, `${gb(mem.used)} af ${gb(mem.limit)} GB brugt. Agenterne kan gå i stå; giv containeren mere RAM eller stop et team.`)));
}
const waiting = snap.rigs.flatMap((r) => r.seats.filter((s) => s.waiting || s.stalled));
const gates = snap.queue.filter((q) => q.gate);
if (waiting.length || gates.length) {
view.push($('section', { class: 'wt-card wt-card--raised wt-stack' },
@@ -160,7 +175,7 @@ function seatView(name) {
function render() {
if (!snap) return;
document.getElementById('updated').textContent = ago(snap.updated);
document.getElementById('updated').textContent = ago(snap.updated) + (snap.memory ? ` · ${gb(snap.memory.used)}/${gb(snap.memory.limit)} GB` : '');
const m = location.hash.match(/^#\/seat\/(.+)$/);
const view = document.getElementById('view');
// keep the open seat view, and never wipe an answer being typed