@kontextmind/kxm
Version:
KXM local-first multi-agent orchestration and operator dashboard
379 lines (353 loc) • 16.7 kB
text/typescript
import { CloudTokenError, hubBindingScope, type HubBindingScope } from "./hub-binding.ts";
export const TENANT_STATUS_SCHEMA = "kxm.tenant-status.v1" as const;
/**
* One upstream of the tenant view, with its own reachability. A source that could not be
* read is reported as `unavailable` with a stable reason — never silently filled from the
* other source, because the two sources answer different questions: the hub holds
* *metadata* (agent roster, message queue, its own workflow runs), the Runtime holds the
* *authoritative* run state on the box that owns the run. Blurring them is how a portal
* ends up rendering one as the other.
*/
export type TenantSourceState = "ok" | "unavailable";
export interface TenantSource<T> {
state: TenantSourceState;
/** When this source was read (or attempted), so a consumer can age the data itself. */
observedAt: string;
/** Stable machine code (`hub_unreachable`, `hub_timeout`, `runtime_supervisor_not_running`, …). */
reason?: string;
value?: T;
}
/** Hub-side roster entry: presence and liveness, per the hub's own observation. */
export interface TenantHubAgent {
id: string;
name: string;
online: boolean;
}
export interface TenantHubStage {
id: string;
status: string;
attempts?: number | undefined;
}
/**
* A run as the hub records it. The hub mints its own run ids at workflow start; this is a
* hub workflow run — metadata about work, not the Runtime's state for it.
*/
export interface TenantHubRun {
id: string;
status: string;
definitionId: string;
currentStage?: string | undefined;
targetAgentName?: string | undefined;
updatedAt?: string | undefined;
progress?: { done: number; total: number };
stages?: TenantHubStage[];
source: "hub-projection";
}
export interface TenantHubPlan {
id: string;
runId: string;
summary: string;
createdAt: string;
stageId?: string | undefined;
severity?: string | undefined;
}
export interface TenantHubValue {
project: string;
fetchedAt: string;
agents: TenantHubAgent[];
openMessageTotal: number;
runTotal: number;
runs: TenantHubRun[];
plans: TenantHubPlan[];
}
/** A run as the Runtime that owns it records it. This is the authoritative state. */
export interface TenantRuntimeRun {
runId: string;
status: string;
homeRuntimeId: string;
workflowId?: string | undefined;
createdAt?: string | undefined;
updatedAt?: string | undefined;
/** Set when the stored row exists but the event-log fold refused; `status` is then the
* cached row, and a consumer must not present it as folded state. */
projectionError?: string | undefined;
/** `runtime-cached` exactly when `projectionError` is set: the row is the cache, not the
* folded state, and neither the label nor the cross-check may treat it as authoritative. */
source: "runtime-authoritative" | "runtime-cached";
}
export interface TenantRuntimeValue {
runs: TenantRuntimeRun[];
}
export interface TenantRunDiscrepancy {
runId: string;
hubStatus: string;
runtimeStatus: string;
}
/**
* Cross-check between the two run populations. Hub runs and Runtime runs mint their ids
* independently (`newId("run")` on each side), so an id appearing in both means a hub run
* that this box drove — and only then is a status comparison meaningful. With no overlap
* the comparison is **unverified, not agreed**: "no matching ids" says the populations did
* not intersect, nothing more, and reporting agreement there would fabricate a green light
* out of an empty set.
*/
export interface TenantRunComparison {
state: "compared" | "unverified" | "unavailable";
reason?: string;
matched?: number;
/** Shared ids whose Runtime row failed to fold: present in both stores, but the
* authoritative side could not be read, so those runs are unverified rather than agreed. */
unverifiedFoldRuns?: number;
discrepancies?: TenantRunDiscrepancy[];
}
export interface TenantStatusPayload {
schema: typeof TENANT_STATUS_SCHEMA;
project: string;
generatedAt: string;
hubUrl: string;
/** Reuses the `kxm hub bind` classification: `loopback` and `remote` ship different rules. */
bindingScope: "loopback" | "remote";
hub: TenantSource<TenantHubValue>;
runtime: TenantSource<TenantRuntimeValue>;
runComparison: TenantRunComparison;
/** True when any source is unavailable. Assembling a payload is not the same as seeing. */
degraded: boolean;
}
export interface TenantStatusRuntimeReader {
listRuns(): Promise<TenantRuntimeRun[]>;
}
const DEFAULT_HUB_TIMEOUT_MS = 5_000;
function hubSourceReason(status: number): string {
if (status === 401 || status === 403) return "hub_unauthorized";
if (status === 404) return "hub_not_found";
return `hub_http_${status}`;
}
function asString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
* Compose the tenant view from the two authorities that actually hold the data.
*
* Both sources are read **concurrently and independently**: a hub that hangs to its
* deadline cannot delay or suppress the Runtime half of the view, and a Runtime that is
* not running cannot take the hub metadata down with it. The hub is read over HTTP
* (`/v1/ops/snapshot`, admin credential); the Runtime is read through an injected reader
* so this function stays pure and testable — the CLI passes a reader that attaches to the
* live supervisor, and a test passes whatever it likes.
*/
export async function assembleTenantStatus(input: {
project: string;
hubUrl: string;
/** Resolves the admin credential. May throw on a malformed persisted record; that
* failure belongs to the hub source alone and never aborts the Runtime read. */
resolveAdminToken: () => string | undefined;
/** When set, this is the scope already classified for `hubUrl` (cloud forwards included). */
bindingScope?: HubBindingScope | undefined;
fetchImpl: typeof fetch;
runtime: TenantStatusRuntimeReader;
hubTimeoutMs?: number | undefined;
now?: () => Date;
}): Promise<TenantStatusPayload> {
const now = input.now ?? ((): Date => new Date());
const base = input.hubUrl.replace(/\/$/, "");
const at = (): string => now().toISOString();
const readHub = async (): Promise<TenantSource<TenantHubValue>> => {
const attemptedAt = at();
let token: string | undefined;
try {
token = input.resolveAdminToken();
} catch (error) {
// A malformed hub-env record is a configuration failure with a specific repair.
// It belongs to this source; the Runtime read proceeds regardless.
// A cloud binding that cannot produce a token is not that failure: the local
// hub-env token was not consulted.
if (error instanceof CloudTokenError) {
return { state: "unavailable", observedAt: attemptedAt, reason: error.code };
}
void error;
return { state: "unavailable", observedAt: attemptedAt, reason: "hub_credential_unreadable" };
}
try {
const response = await input.fetchImpl(`${base}/v1/ops/snapshot?project=${encodeURIComponent(input.project)}`, {
headers: token ? { authorization: `Bearer ${token}` } : {},
signal: AbortSignal.timeout(input.hubTimeoutMs ?? DEFAULT_HUB_TIMEOUT_MS),
});
if (!response.ok) {
return { state: "unavailable", observedAt: attemptedAt, reason: hubSourceReason(response.status) };
}
let body: unknown;
try {
body = await response.json();
} catch (error) {
// A deadline can expire mid-body; that is a timeout, not malformed content, and
// classifying it as `hub_response_invalid` would hide the one fact that matters.
const name = error instanceof Error ? error.name : undefined;
if (name === "TimeoutError" || name === "AbortError") {
return { state: "unavailable", observedAt: attemptedAt, reason: "hub_timeout" };
}
// Reachable but not a JSON document: a different failure than a dead socket.
return { state: "unavailable", observedAt: attemptedAt, reason: "hub_response_invalid" };
}
if (!isObject(body)
|| typeof body.project !== "string"
|| typeof body.fetchedAt !== "string"
|| !Array.isArray(body.agents)
|| !Array.isArray(body.runs)
|| !Array.isArray(body.plans ?? [])
|| typeof body.openMessageTotal !== "number"
|| typeof body.runTotal !== "number") {
// A 200 with an unusable body must not become a healthy empty snapshot.
return { state: "unavailable", observedAt: attemptedAt, reason: "hub_response_invalid" };
}
if (body.project !== input.project) {
return { state: "unavailable", observedAt: attemptedAt, reason: "hub_project_mismatch" };
}
const runRows = body.runs.filter(isObject);
const value: TenantHubValue = {
project: body.project,
fetchedAt: body.fetchedAt,
agents: body.agents.filter(isObject).map((agent) => ({
id: asString(agent.id) ?? "",
name: asString(agent.name) ?? "",
online: agent.online === true,
})),
openMessageTotal: body.openMessageTotal,
runTotal: body.runTotal,
runs: runRows.map((run) => {
const stages = Array.isArray(run.stages)
? run.stages.filter(isObject).map((stage) => ({
id: asString(stage.id) ?? "",
status: asString(stage.status) ?? "unknown",
...(typeof stage.attempts === "number" ? { attempts: stage.attempts } : {}),
}))
: undefined;
const done = stages?.filter((stage) => stage.status === "passed" || stage.status === "failed" || stage.status === "warning").length;
return {
id: asString(run.id) ?? "",
status: asString(run.status) ?? "unknown",
definitionId: asString(run.definitionId) ?? "",
...(asString(run.currentStage) ? { currentStage: asString(run.currentStage) } : {}),
...(asString(run.targetAgentName) ? { targetAgentName: asString(run.targetAgentName) } : {}),
...(asString(run.updatedAt) ? { updatedAt: asString(run.updatedAt) } : {}),
...(stages !== undefined ? { progress: { done: done ?? 0, total: stages.length }, stages } : {}),
source: "hub-projection" as const,
};
}),
plans: (Array.isArray(body.plans) ? body.plans : []).filter(isObject).map((plan) => ({
id: asString(plan.id) ?? "",
runId: asString(plan.runId) ?? "",
summary: asString(plan.summary) ?? "",
createdAt: asString(plan.createdAt) ?? "",
...(asString(plan.stageId) ? { stageId: asString(plan.stageId) } : {}),
...(asString(plan.severity) ? { severity: asString(plan.severity) } : {}),
})),
};
return { state: "ok", observedAt: attemptedAt, value };
} catch (error) {
const name = error instanceof Error ? error.name : undefined;
if (name === "TimeoutError" || name === "AbortError") {
return { state: "unavailable", observedAt: attemptedAt, reason: "hub_timeout" };
}
return { state: "unavailable", observedAt: attemptedAt, reason: "hub_unreachable" };
}
};
const readRuntime = async (): Promise<TenantSource<TenantRuntimeValue>> => {
const attemptedAt = at();
try {
const runs = await input.runtime.listRuns();
return { state: "ok", observedAt: attemptedAt, value: { runs } };
} catch (error) {
const code = error instanceof Error && "issues" in error
&& Array.isArray((error as { issues: Array<{ code?: unknown }> }).issues)
&& typeof (error as { issues: Array<{ code?: unknown }> }).issues[0]?.code === "string"
? (error as { issues: Array<{ code: string }> }).issues[0]!.code
: undefined;
return { state: "unavailable", observedAt: attemptedAt, reason: code ?? "runtime_unavailable" };
}
};
const [hub, runtime] = await Promise.all([readHub(), readRuntime()]);
let runComparison: TenantRunComparison;
if (!hub.value || !runtime.value) {
runComparison = {
state: "unavailable",
reason: !hub.value ? `hub_${hub.reason ?? "unavailable"}` : `runtime_${runtime.reason ?? "unavailable"}`,
};
} else {
// Only cleanly folded rows are authoritative. A row whose fold failed is the cache, and
// counting it as agreement is precisely the lie this comparison exists to prevent: a
// corrupt event log under a hub run's id would otherwise print "agree".
const authoritative = new Map(
runtime.value.runs
.filter((run) => run.projectionError === undefined)
.map((run) => [run.runId, run.status] as const),
);
const hubIds = new Set(hub.value.runs.map((run) => run.id));
const foldFailed = runtime.value.runs.filter((run) => run.projectionError !== undefined && hubIds.has(run.runId));
const discrepancies: TenantRunDiscrepancy[] = [];
let matched = 0;
for (const projected of hub.value.runs) {
const runtimeStatus = authoritative.get(projected.id);
if (runtimeStatus === undefined) continue;
matched += 1;
if (runtimeStatus !== projected.status) {
discrepancies.push({ runId: projected.id, hubStatus: projected.status, runtimeStatus });
}
}
if (matched > 0) {
runComparison = {
state: "compared",
matched,
...(foldFailed.length > 0 ? { unverifiedFoldRuns: foldFailed.length } : {}),
...(discrepancies.length > 0 ? { discrepancies } : {}),
};
} else {
runComparison = foldFailed.length > 0
? { state: "unverified", reason: "runtime_fold_failed", matched: 0, unverifiedFoldRuns: foldFailed.length }
: { state: "unverified", reason: "run_identity_link_absent", matched: 0 };
}
}
return {
schema: TENANT_STATUS_SCHEMA,
project: input.project,
generatedAt: at(),
hubUrl: input.hubUrl,
bindingScope: input.bindingScope ?? hubBindingScope(input.hubUrl),
hub,
runtime,
runComparison,
degraded: hub.state !== "ok" || runtime.state !== "ok",
};
}
/** One line per source for humans; the portal reads the JSON, operators read this. */
export function formatTenantStatus(payload: TenantStatusPayload): string {
const hubLine = payload.hub.state === "ok" && payload.hub.value
? `hub ${payload.hub.value.agents.filter((agent) => agent.online).length}/${payload.hub.value.agents.length} agents online, ${payload.hub.value.runs.length} hub runs, ${payload.hub.value.openMessageTotal} open messages`
: `hub unavailable (${payload.hub.reason ?? "unknown"})`;
const runtimeLine = payload.runtime.state === "ok" && payload.runtime.value
? (() => {
const cached = payload.runtime.value!.runs.filter((run) => run.projectionError !== undefined).length;
const authoritative = payload.runtime.value!.runs.length - cached;
// A fold-failed row is the cache; printing it under "authoritative" would put the
// lie back in exactly the place an operator reads first.
return `runtime ${authoritative} runs (authoritative, on this box)${cached > 0 ? `, ${cached} cached (fold failed, not state)` : ""}`;
})()
: `runtime unavailable (${payload.runtime.reason ?? "unknown"})`;
const comparisonLine = payload.runComparison.state === "compared"
? payload.runComparison.discrepancies && payload.runComparison.discrepancies.length > 0
? `cross-check: ${payload.runComparison.discrepancies.length} of ${payload.runComparison.matched} matched run(s) disagree${payload.runComparison.unverifiedFoldRuns ? ` (${payload.runComparison.unverifiedFoldRuns} unverified: fold failed)` : ""}`
: `cross-check: ${payload.runComparison.matched} matched run(s) agree${payload.runComparison.unverifiedFoldRuns ? ` (${payload.runComparison.unverifiedFoldRuns} unverified: fold failed)` : ""}`
: payload.runComparison.state === "unverified"
? payload.runComparison.reason === "runtime_fold_failed"
? `cross-check: unavailable — ${payload.runComparison.unverifiedFoldRuns ?? 0} shared run(s) could not be verified (runtime fold failed)`
: "cross-check: unavailable — no run id appears in both sources (independent id spaces)"
: `cross-check: unavailable (${payload.runComparison.reason ?? "unknown"})`;
return [
`tenant ${payload.project} @ ${payload.hubUrl} (${payload.bindingScope})`,
hubLine,
runtimeLine,
comparisonLine,
].join("\n");
}