Show agent liveness, stuck/zombie detection and parse errors in admin
This commit is contained in:
Vendored
+65
-6
@@ -1,7 +1,7 @@
|
||||
import { get } from "./api.js";
|
||||
import { ago, errorText, h, replace } from "./dom.js";
|
||||
import { findTask, on, onEach, state } from "./state.js";
|
||||
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created"]);
|
||||
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created", "admin.health"]);
|
||||
export function mountAdmin(root) {
|
||||
let status;
|
||||
let error = "";
|
||||
@@ -13,7 +13,10 @@ export function mountAdmin(root) {
|
||||
replace(root, h("div", { class: error ? "empty error-text" : "empty" }, error || "Loading…"));
|
||||
return;
|
||||
}
|
||||
replace(root, h("div", { class: "admin-view" }, error && h("p", { class: "error-text" }, error), turnsSection(status), rateLimitsSection(status.rate_limits), serverSection(status)));
|
||||
const scrollTop = root.querySelector(".admin-view")?.scrollTop ?? 0;
|
||||
const open = new Set([...root.querySelectorAll(".problems details[open]")].map((d) => d.dataset["vendor"] ?? ""));
|
||||
replace(root, h("div", { class: "admin-view" }, error && h("p", { class: "error-text" }, error), problemsSection(status, open), turnsSection(status), rateLimitsSection(status.rate_limits), serverSection(status)));
|
||||
root.querySelector(".admin-view").scrollTop = scrollTop;
|
||||
};
|
||||
const refresh = async () => {
|
||||
if (inFlight) {
|
||||
@@ -45,8 +48,20 @@ export function mountAdmin(root) {
|
||||
void refresh();
|
||||
return;
|
||||
}
|
||||
if (status && ev.seq > status.last_event_seq) {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
let changed = false;
|
||||
const proc = status.agent_processes.find((p) => p.session_id === ev.agent_session_id);
|
||||
if (proc && Date.parse(ev.created_at) - Date.parse(proc.last_event_at) >= 1000) {
|
||||
proc.last_event_at = ev.created_at;
|
||||
changed = true;
|
||||
}
|
||||
if (ev.seq > status.last_event_seq) {
|
||||
status.last_event_seq = ev.seq;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
render();
|
||||
}
|
||||
};
|
||||
@@ -71,19 +86,63 @@ export function mountAdmin(root) {
|
||||
function turnsSection(s) {
|
||||
const vendors = [...new Set([...Object.keys(s.caps.per_vendor), ...s.running_turns.map((t) => t.vendor), ...s.queued_turns.map((t) => t.vendor)])].sort();
|
||||
const runningFor = (vendor) => s.running_turns.filter((t) => t.vendor === vendor).length;
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Turns"), h("div", { class: "caps" }, capTile("All vendors", s.running_turns.length, s.caps.global), ...vendors.map((v) => capTile(v, runningFor(v), s.caps.per_vendor[v])), h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, "Queued"), h("div", { class: s.queued_turns.length > 0 ? "cap-value warn" : "cap-value" }, s.queued_turns.length))), h("h3", {}, "Running"), turnList(s.running_turns, "running", "No turns running."), h("h3", {}, "Queued"), turnList(s.queued_turns, "queued", "Nothing queued."));
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Turns"), h("div", { class: "caps" }, capTile("All vendors", s.running_turns.length, s.caps.global), ...vendors.map((v) => capTile(v, runningFor(v), s.caps.per_vendor[v])), h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, "Queued"), h("div", { class: s.queued_turns.length > 0 ? "cap-value warn" : "cap-value" }, s.queued_turns.length))), h("h3", {}, "Running"), turnList(s.running_turns, "running", "No turns running.", s.agent_processes), h("h3", {}, "Queued"), turnList(s.queued_turns, "queued", "Nothing queued.", []));
|
||||
}
|
||||
function capTile(label, running, cap) {
|
||||
const full = cap !== undefined && running >= cap;
|
||||
return h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, label), h("div", { class: full ? "cap-value warn" : "cap-value" }, running, h("span", { class: "cap-max" }, cap === undefined ? " / no cap" : ` / ${cap}`)));
|
||||
}
|
||||
function turnList(turns, kind, empty) {
|
||||
function turnList(turns, kind, empty, procs) {
|
||||
return h("ul", { class: "task-list" }, ...turns.map((t, i) => {
|
||||
const task = findTask(t.task_id);
|
||||
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
|
||||
return h("li", {}, h("a", { href: `#/task/${t.task_id}` }, kind === "queued" ? h("span", { class: "queue-pos" }, `${i + 1}.`) : h("span", { class: "dot running", title: "running" }), h("span", { class: "task-title" }, project && h("span", { class: "project-name" }, `${project.name} ▸ `), task?.title ?? t.task_id), h("span", { class: "task-state" }, t.vendor)));
|
||||
return h("li", {}, h("a", { href: `#/task/${t.task_id}` }, kind === "queued" ? h("span", { class: "queue-pos" }, `${i + 1}.`) : h("span", { class: "dot running", title: "running" }), h("span", { class: "task-title" }, project && h("span", { class: "project-name" }, `${project.name} ▸ `), task?.title ?? t.task_id), liveness(procs.find((p) => p.session_id === t.session_id)), h("span", { class: "task-state" }, t.vendor)));
|
||||
}), turns.length === 0 && h("li", { class: "empty-row" }, empty));
|
||||
}
|
||||
function taskLink(taskId) {
|
||||
if (!taskId) {
|
||||
return "—";
|
||||
}
|
||||
const task = findTask(taskId);
|
||||
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
|
||||
return h("a", { href: `#/task/${taskId}` }, project ? `${project.name} ▸ ` : "", task?.title ?? taskId);
|
||||
}
|
||||
function since(iso) {
|
||||
const seconds = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 1000));
|
||||
if (seconds < 60) {
|
||||
return `${seconds}s ago`;
|
||||
}
|
||||
if (seconds < 3600) {
|
||||
return `${Math.floor(seconds / 60)}m ${seconds % 60}s ago`;
|
||||
}
|
||||
return ago(iso);
|
||||
}
|
||||
function liveness(p) {
|
||||
if (!p) {
|
||||
return false;
|
||||
}
|
||||
if (p.health === "ok") {
|
||||
return h("span", { class: "task-state" }, since(p.last_event_at));
|
||||
}
|
||||
return h("span", { class: "task-state danger" }, `${p.health} · ${since(p.last_event_at)}`);
|
||||
}
|
||||
const zombieLabel = {
|
||||
running_without_process: "Running with no process",
|
||||
process_without_session: "Process with no session",
|
||||
};
|
||||
function problemsSection(s, openVendors) {
|
||||
const pe = s.protocol_errors;
|
||||
const vendors = Object.keys(pe.counts).sort();
|
||||
if (s.zombies.length === 0 && vendors.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Problems"), h("ul", { class: "problems" }, ...s.zombies.map((z) => h("li", {}, h("span", { class: "danger" }, zombieLabel[z.kind]), " ", z.task_id ? taskLink(z.task_id) : "", " ", h("span", { class: "hint" }, z.detail))), ...vendors.map((v) => {
|
||||
const latest = pe.recent.find((e) => e.vendor === v);
|
||||
const n = pe.counts[v] ?? 0;
|
||||
return h("li", {}, h("span", { class: "danger" }, `${v}: ${n} bad stream line${n === 1 ? "" : "s"}`), latest &&
|
||||
h("details", { data: { vendor: v }, open: openVendors.has(v) }, h("summary", {}, `latest ${since(latest.at)}: ${latest.reason}`), h("pre", { class: "raw-line" }, latest.raw, new TextEncoder().encode(latest.raw).length < latest.size ? `… (${bytes(latest.size)})` : "")));
|
||||
})));
|
||||
}
|
||||
function rateLimitsSection(limits) {
|
||||
const vendors = Object.keys(limits).sort();
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Rate limits"), ...vendors.map((v) => rateLimitCard(v, limits[v] ?? {})), vendors.length === 0 && h("p", { class: "hint" }, "No vendor has reported a rate-limit status since byre started."));
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+1
@@ -110,6 +110,7 @@ export function handleEvent(ev) {
|
||||
if (ev.task_id && ev.task_id === state.selectedTaskId && !liveDuringLoad) {
|
||||
notify("stream", ev);
|
||||
}
|
||||
notify("event", ev);
|
||||
return;
|
||||
}
|
||||
if (ev.task_id && ev.task_id === state.selectedTaskId) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+92
-6
@@ -1,8 +1,8 @@
|
||||
import { get, type ByreEvent, type Status, type TurnRef } from "./api.js";
|
||||
import { get, type AgentProcess, type ByreEvent, type Status, type TurnRef, type Zombie } from "./api.js";
|
||||
import { ago, errorText, h, replace } from "./dom.js";
|
||||
import { findTask, on, onEach, state } from "./state.js";
|
||||
|
||||
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created"]);
|
||||
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created", "admin.health"]);
|
||||
|
||||
export function mountAdmin(root: HTMLElement): () => void {
|
||||
let status: Status | undefined;
|
||||
@@ -16,17 +16,21 @@ export function mountAdmin(root: HTMLElement): () => void {
|
||||
replace(root, h("div", { class: error ? "empty error-text" : "empty" }, error || "Loading…"));
|
||||
return;
|
||||
}
|
||||
const scrollTop = root.querySelector(".admin-view")?.scrollTop ?? 0;
|
||||
const open = new Set([...root.querySelectorAll<HTMLDetailsElement>(".problems details[open]")].map((d) => d.dataset["vendor"] ?? ""));
|
||||
replace(
|
||||
root,
|
||||
h(
|
||||
"div",
|
||||
{ class: "admin-view" },
|
||||
error && h("p", { class: "error-text" }, error),
|
||||
problemsSection(status, open),
|
||||
turnsSection(status),
|
||||
rateLimitsSection(status.rate_limits),
|
||||
serverSection(status),
|
||||
),
|
||||
);
|
||||
root.querySelector(".admin-view")!.scrollTop = scrollTop;
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
@@ -59,8 +63,20 @@ export function mountAdmin(root: HTMLElement): () => void {
|
||||
void refresh();
|
||||
return;
|
||||
}
|
||||
if (status && ev.seq > status.last_event_seq) {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
let changed = false;
|
||||
const proc = status.agent_processes.find((p) => p.session_id === ev.agent_session_id);
|
||||
if (proc && Date.parse(ev.created_at) - Date.parse(proc.last_event_at) >= 1000) {
|
||||
proc.last_event_at = ev.created_at;
|
||||
changed = true;
|
||||
}
|
||||
if (ev.seq > status.last_event_seq) {
|
||||
status.last_event_seq = ev.seq;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
render();
|
||||
}
|
||||
};
|
||||
@@ -102,9 +118,9 @@ function turnsSection(s: Status): HTMLElement {
|
||||
h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, "Queued"), h("div", { class: s.queued_turns.length > 0 ? "cap-value warn" : "cap-value" }, s.queued_turns.length)),
|
||||
),
|
||||
h("h3", {}, "Running"),
|
||||
turnList(s.running_turns, "running", "No turns running."),
|
||||
turnList(s.running_turns, "running", "No turns running.", s.agent_processes),
|
||||
h("h3", {}, "Queued"),
|
||||
turnList(s.queued_turns, "queued", "Nothing queued."),
|
||||
turnList(s.queued_turns, "queued", "Nothing queued.", []),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,7 +134,7 @@ function capTile(label: string, running: number, cap: number | undefined): HTMLE
|
||||
);
|
||||
}
|
||||
|
||||
function turnList(turns: TurnRef[], kind: "running" | "queued", empty: string): HTMLElement {
|
||||
function turnList(turns: TurnRef[], kind: "running" | "queued", empty: string, procs: AgentProcess[]): HTMLElement {
|
||||
return h(
|
||||
"ul",
|
||||
{ class: "task-list" },
|
||||
@@ -133,6 +149,7 @@ function turnList(turns: TurnRef[], kind: "running" | "queued", empty: string):
|
||||
{ href: `#/task/${t.task_id}` },
|
||||
kind === "queued" ? h("span", { class: "queue-pos" }, `${i + 1}.`) : h("span", { class: "dot running", title: "running" }),
|
||||
h("span", { class: "task-title" }, project && h("span", { class: "project-name" }, `${project.name} ▸ `), task?.title ?? t.task_id),
|
||||
liveness(procs.find((p) => p.session_id === t.session_id)),
|
||||
h("span", { class: "task-state" }, t.vendor),
|
||||
),
|
||||
);
|
||||
@@ -141,6 +158,75 @@ function turnList(turns: TurnRef[], kind: "running" | "queued", empty: string):
|
||||
);
|
||||
}
|
||||
|
||||
function taskLink(taskId: string | undefined): HTMLElement | string {
|
||||
if (!taskId) {
|
||||
return "—";
|
||||
}
|
||||
const task = findTask(taskId);
|
||||
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
|
||||
return h("a", { href: `#/task/${taskId}` }, project ? `${project.name} ▸ ` : "", task?.title ?? taskId);
|
||||
}
|
||||
|
||||
function since(iso: string): string {
|
||||
const seconds = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 1000));
|
||||
if (seconds < 60) {
|
||||
return `${seconds}s ago`;
|
||||
}
|
||||
if (seconds < 3600) {
|
||||
return `${Math.floor(seconds / 60)}m ${seconds % 60}s ago`;
|
||||
}
|
||||
return ago(iso);
|
||||
}
|
||||
|
||||
function liveness(p: AgentProcess | undefined): HTMLElement | false {
|
||||
if (!p) {
|
||||
return false;
|
||||
}
|
||||
if (p.health === "ok") {
|
||||
return h("span", { class: "task-state" }, since(p.last_event_at));
|
||||
}
|
||||
return h("span", { class: "task-state danger" }, `${p.health} · ${since(p.last_event_at)}`);
|
||||
}
|
||||
|
||||
const zombieLabel: Record<Zombie["kind"], string> = {
|
||||
running_without_process: "Running with no process",
|
||||
process_without_session: "Process with no session",
|
||||
};
|
||||
|
||||
function problemsSection(s: Status, openVendors: Set<string>): HTMLElement | false {
|
||||
const pe = s.protocol_errors;
|
||||
const vendors = Object.keys(pe.counts).sort();
|
||||
if (s.zombies.length === 0 && vendors.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return h(
|
||||
"section",
|
||||
{ class: "admin-section" },
|
||||
h("h2", {}, "Problems"),
|
||||
h(
|
||||
"ul",
|
||||
{ class: "problems" },
|
||||
...s.zombies.map((z) => h("li", {}, h("span", { class: "danger" }, zombieLabel[z.kind]), " ", z.task_id ? taskLink(z.task_id) : "", " ", h("span", { class: "hint" }, z.detail))),
|
||||
...vendors.map((v) => {
|
||||
const latest = pe.recent.find((e) => e.vendor === v);
|
||||
const n = pe.counts[v] ?? 0;
|
||||
return h(
|
||||
"li",
|
||||
{},
|
||||
h("span", { class: "danger" }, `${v}: ${n} bad stream line${n === 1 ? "" : "s"}`),
|
||||
latest &&
|
||||
h(
|
||||
"details",
|
||||
{ data: { vendor: v }, open: openVendors.has(v) },
|
||||
h("summary", {}, `latest ${since(latest.at)}: ${latest.reason}`),
|
||||
h("pre", { class: "raw-line" }, latest.raw, new TextEncoder().encode(latest.raw).length < latest.size ? `… (${bytes(latest.size)})` : ""),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function rateLimitsSection(limits: Status["rate_limits"]): HTMLElement {
|
||||
const vendors = Object.keys(limits).sort();
|
||||
return h(
|
||||
|
||||
@@ -732,6 +732,13 @@ export interface components {
|
||||
Status: {
|
||||
/** @description Agent processes currently running. */
|
||||
live_agents: number;
|
||||
/** @description Every agent process byre is tracking, idle sessions included. */
|
||||
agent_processes: components["schemas"]["AgentProcess"][];
|
||||
/** @description Sessions marked running with no live process, and agent processes byre has no session for. */
|
||||
zombies: components["schemas"]["Zombie"][];
|
||||
/** @description How long a running turn may go without a stream event before it counts as stuck. */
|
||||
stuck_after_seconds: number;
|
||||
protocol_errors: components["schemas"]["ProtocolErrors"];
|
||||
/** @description Turns running now, oldest session first. */
|
||||
running_turns: components["schemas"]["TurnRef"][];
|
||||
/** @description Turns waiting for room under the caps, in the order they will start. */
|
||||
@@ -764,6 +771,51 @@ export interface components {
|
||||
task_id: string;
|
||||
vendor: string;
|
||||
};
|
||||
AgentProcess: {
|
||||
session_id: string;
|
||||
task_id: string;
|
||||
vendor: string;
|
||||
pid: number;
|
||||
/** @description The session's state */
|
||||
state: string;
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description When the process last wrote a line to its stream
|
||||
*/
|
||||
last_event_at: string;
|
||||
/**
|
||||
* @description stuck means a turn is running and no stream event has arrived for stuck_after_seconds; exited means the process is gone.
|
||||
* @enum {string}
|
||||
*/
|
||||
health: "ok" | "stuck" | "exited";
|
||||
};
|
||||
Zombie: {
|
||||
/** @enum {string} */
|
||||
kind: "running_without_process" | "process_without_session";
|
||||
detail: string;
|
||||
session_id?: string;
|
||||
task_id?: string;
|
||||
pid?: number;
|
||||
};
|
||||
ProtocolErrors: {
|
||||
/** @description Stream lines the adapter couldn't parse or didn't recognise since byre started, keyed by vendor. */
|
||||
counts: {
|
||||
[key: string]: number;
|
||||
};
|
||||
/** @description The most recent ones, newest first. */
|
||||
recent: components["schemas"]["ProtocolError"][];
|
||||
};
|
||||
ProtocolError: {
|
||||
vendor: string;
|
||||
session_id: string;
|
||||
/** Format: date-time */
|
||||
at: string;
|
||||
reason: string;
|
||||
/** @description The line as received */
|
||||
raw: string;
|
||||
/** @description The line's full length in bytes. */
|
||||
size: number;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Error. */
|
||||
|
||||
@@ -14,6 +14,9 @@ export type ByreEvent = Schemas["Event"];
|
||||
export type Status = Schemas["Status"];
|
||||
export type NotificationPrefs = Schemas["NotificationPrefs"];
|
||||
export type TurnRef = Schemas["TurnRef"];
|
||||
export type AgentProcess = Schemas["AgentProcess"];
|
||||
export type Zombie = Schemas["Zombie"];
|
||||
export type ProtocolErrors = Schemas["ProtocolErrors"];
|
||||
|
||||
type Method = "get" | "post" | "put" | "delete";
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ export function handleEvent(ev: ByreEvent): void {
|
||||
if (ev.task_id && ev.task_id === state.selectedTaskId && !liveDuringLoad) {
|
||||
notify("stream", ev);
|
||||
}
|
||||
notify("event", ev);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+28
-1
@@ -1385,7 +1385,9 @@ pre.diff .meta {
|
||||
}
|
||||
|
||||
.rl-status.danger,
|
||||
.rl-windows .danger {
|
||||
.rl-windows .danger,
|
||||
.problems .danger,
|
||||
.task-state.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
@@ -1402,6 +1404,31 @@ pre.diff .meta {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.problems {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.problems li {
|
||||
padding: 2px 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.problems summary {
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.raw-line {
|
||||
margin: 4px 0 0;
|
||||
font-size: 11px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 12em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.rl-windows th {
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
|
||||
Reference in New Issue
Block a user