summaryrefslogtreecommitdiffhomepage
path: root/src/features/concurrency/logic
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/concurrency/logic')
-rw-r--r--src/features/concurrency/logic/types.ts82
-rw-r--r--src/features/concurrency/logic/view-model.test.ts366
-rw-r--r--src/features/concurrency/logic/view-model.ts266
3 files changed, 714 insertions, 0 deletions
diff --git a/src/features/concurrency/logic/types.ts b/src/features/concurrency/logic/types.ts
new file mode 100644
index 0000000..f05211f
--- /dev/null
+++ b/src/features/concurrency/logic/types.ts
@@ -0,0 +1,82 @@
+import type {
+ ConcurrencyLimitResponse,
+ ConcurrencyLimitsResponse,
+ ConcurrencyStatusEntry,
+ ConcurrencyStatusResponse,
+ SetConcurrencyLimitRequest,
+} from "@dispatch/transport-contract";
+
+/**
+ * Pure core types for the concurrency feature — zero DOM, zero effects, zero
+ * Svelte.
+ *
+ * The backend tracks + limits how many concurrent token-generating API requests
+ * are in flight PER PROVIDER. When the cap is reached, additional requests queue
+ * and are granted slots oldest-agent-first; a 429 backoff PAUSES a provider's
+ * queue until `pausedUntil`. The cap is in-memory + per-provider (no persistence),
+ * managed via a plain REST surface under `/concurrency/...` provided by the
+ * `concurrency` extension. When the extension isn't loaded, the list + status
+ * endpoints return empty arrays (`{ limits: [] }` / `{ providers: [] }`); the
+ * single / PUT / DELETE endpoints return `503`.
+ *
+ * The data shapes ARE part of `@dispatch/transport-contract` (0.23.0), so they
+ * are imported directly (mirrors `mcp` / `computer`). The result types + injected
+ * ports below are FE-owned (the composition root adapts the store's HTTP calls to
+ * them). The endpoints are GLOBAL (not workspace- or conversation-scoped).
+ */
+
+/** Re-export the contract shapes so consumers import a single surface. */
+export type {
+ ConcurrencyLimitResponse,
+ ConcurrencyLimitsResponse,
+ ConcurrencyStatusEntry,
+ ConcurrencyStatusResponse,
+ SetConcurrencyLimitRequest,
+};
+
+/**
+ * A configured concurrency limit — one provider's cap on in-flight requests.
+ * Same shape as the contract's `ConcurrencyLimitResponse`.
+ */
+export interface ConcurrencyLimitEntry {
+ readonly providerId: string;
+ readonly limit: number;
+}
+
+// ── Result types (port outcomes; the store returns these directly) ──────────────
+
+/** Outcome of `GET /concurrency/limits` (all configured limits). */
+export type ConcurrencyLimitsResult =
+ | { readonly ok: true; readonly limits: readonly ConcurrencyLimitEntry[] }
+ | { readonly ok: false; readonly error: string };
+
+/**
+ * Outcome of `GET`/`PUT /concurrency/limits/:providerId` — the configured limit
+ * for one provider. `GET` returns `404` when the provider has no limit (surfaced
+ * as `ok: false`); `PUT` returns `400` for a non-positive-integer body.
+ */
+export type ConcurrencyLimitResult =
+ | { readonly ok: true; readonly providerId: string; readonly limit: number }
+ | { readonly ok: false; readonly error: string };
+
+/** Outcome of `DELETE /concurrency/limits/:providerId` (remove → unlimited). */
+export type ConcurrencyDeleteResult =
+ | { readonly ok: true; readonly providerId: string }
+ | { readonly ok: false; readonly error: string };
+
+/** Outcome of `GET /concurrency/status` (live status for every limited provider). */
+export type ConcurrencyStatusResult =
+ | { readonly ok: true; readonly providers: readonly ConcurrencyStatusEntry[] }
+ | { readonly ok: false; readonly error: string };
+
+// ── Injected ports (consumer-defines-port; the composition root adapts the
+// store's HTTP calls to these shapes). ──────────────────────────────────────
+
+export type LoadConcurrencyLimits = () => Promise<ConcurrencyLimitsResult>;
+export type GetConcurrencyLimit = (providerId: string) => Promise<ConcurrencyLimitResult>;
+export type SaveConcurrencyLimit = (
+ providerId: string,
+ limit: number,
+) => Promise<ConcurrencyLimitResult>;
+export type DeleteConcurrencyLimit = (providerId: string) => Promise<ConcurrencyDeleteResult>;
+export type LoadConcurrencyStatus = () => Promise<ConcurrencyStatusResult>;
diff --git a/src/features/concurrency/logic/view-model.test.ts b/src/features/concurrency/logic/view-model.test.ts
new file mode 100644
index 0000000..336a6eb
--- /dev/null
+++ b/src/features/concurrency/logic/view-model.test.ts
@@ -0,0 +1,366 @@
+import type { ConcurrencyStatusEntry } from "@dispatch/transport-contract";
+import { describe, expect, it } from "vitest";
+import {
+ formatPauseDuration,
+ normalizeConcurrencyLimit,
+ normalizeConcurrencyLimits,
+ normalizeConcurrencyStatus,
+ parseLimitInput,
+ pauseLabel,
+ summarizeLimits,
+ summarizeStatus,
+ viewConcurrencyLimit,
+ viewConcurrencyLimits,
+ viewConcurrencyStatus,
+ viewConcurrencyStatuses,
+} from "./view-model";
+
+const status = (over: Partial<ConcurrencyStatusEntry> = {}): ConcurrencyStatusEntry => ({
+ providerId: "umans",
+ limit: 4,
+ inFlight: 2,
+ queued: 0,
+ paused: false,
+ ...over,
+});
+
+// ── parseLimitInput ───────────────────────────────────────────────────────────
+
+describe("parseLimitInput", () => {
+ it("accepts positive integers", () => {
+ expect(parseLimitInput("4")).toBe(4);
+ expect(parseLimitInput(" 12 ")).toBe(12);
+ expect(parseLimitInput("1")).toBe(1);
+ });
+
+ it("rejects zero, negatives, non-integers, and garbage", () => {
+ expect(parseLimitInput("0")).toBeNull();
+ expect(parseLimitInput("-1")).toBeNull();
+ expect(parseLimitInput("4.5")).toBeNull();
+ expect(parseLimitInput("")).toBeNull();
+ expect(parseLimitInput(" ")).toBeNull();
+ expect(parseLimitInput("abc")).toBeNull();
+ expect(parseLimitInput("4abc")).toBeNull();
+ });
+});
+
+// ── pauseLabel + formatPauseDuration ───────────────────────────────────────────
+
+describe("formatPauseDuration", () => {
+ it("formats seconds / minutes+seconds / hours+minutes", () => {
+ expect(formatPauseDuration(30_000)).toBe("30s");
+ expect(formatPauseDuration(65_000)).toBe("1m 05s");
+ expect(formatPauseDuration(3_660_000)).toBe("1h 01m");
+ });
+
+ it("non-positive → resuming", () => {
+ expect(formatPauseDuration(0)).toBe("resuming");
+ expect(formatPauseDuration(-5_000)).toBe("resuming");
+ });
+});
+
+describe("pauseLabel", () => {
+ it("null when not paused", () => {
+ expect(pauseLabel(false, undefined, 0)).toBeNull();
+ expect(pauseLabel(false, 10_000, 0)).toBeNull();
+ });
+
+ it("'paused' (bare) when paused without a usable timestamp", () => {
+ expect(pauseLabel(true, undefined, 0)).toBe("paused");
+ expect(pauseLabel(true, null, 0)).toBe("paused");
+ expect(pauseLabel(true, Number.NaN, 0)).toBe("paused");
+ });
+
+ it("countdown when paused with a future timestamp", () => {
+ const now = 1_000_000;
+ expect(pauseLabel(true, now + 30_000, now)).toBe("paused — resumes in 30s");
+ expect(pauseLabel(true, now + 65_000, now)).toBe("paused — resumes in 1m 05s");
+ });
+
+ it("'paused' (bare) when the timestamp is missing, non-finite, or expired", () => {
+ expect(pauseLabel(true, 0, 1_000)).toBe("paused");
+ expect(pauseLabel(true, 1_000, 2_000)).toBe("paused");
+ expect(pauseLabel(true, Number.NaN, 0)).toBe("paused");
+ // The countdown prefix only appears with a FUTURE timestamp:
+ expect(pauseLabel(true, 2_000, 1_000)).toBe("paused — resumes in 1s");
+ });
+});
+
+// ── viewConcurrencyStatus ──────────────────────────────────────────────────────
+
+describe("viewConcurrencyStatus", () => {
+ it("serving under capacity → success badge, in-flight label, no queue", () => {
+ const v = viewConcurrencyStatus(status({ inFlight: 2, limit: 4, queued: 0 }), 0);
+ expect(v.inFlightLabel).toBe("2/4");
+ expect(v.queuedLabel).toBe("no queue");
+ expect(v.pausedLabel).toBeNull();
+ expect(v.badge).toBe("success");
+ expect(v.busy).toBe(false);
+ });
+
+ it("at capacity with a queue → warning badge + busy (spinner)", () => {
+ const v = viewConcurrencyStatus(status({ inFlight: 4, limit: 4, queued: 3 }), 0);
+ expect(v.inFlightLabel).toBe("4/4");
+ expect(v.queuedLabel).toBe("3 queued");
+ expect(v.badge).toBe("warning");
+ expect(v.busy).toBe(true);
+ });
+
+ it("idle (no in-flight) → neutral badge, not busy", () => {
+ const v = viewConcurrencyStatus(status({ inFlight: 0, limit: 4, queued: 0 }), 0);
+ expect(v.badge).toBe("neutral");
+ expect(v.busy).toBe(false);
+ expect(v.inFlightLabel).toBe("0/4");
+ });
+
+ it("paused → warning badge + pause countdown label", () => {
+ const now = 1_000_000;
+ const v = viewConcurrencyStatus(
+ status({ paused: true, pausedUntil: now + 30_000, inFlight: 4, limit: 4, queued: 3 }),
+ now,
+ );
+ expect(v.paused).toBe(true);
+ expect(v.pausedLabel).toBe("paused — resumes in 30s");
+ expect(v.badge).toBe("warning");
+ expect(v.busy).toBe(true);
+ });
+
+ it("at capacity but no queue → success (busy only when queuing)", () => {
+ const v = viewConcurrencyStatus(status({ inFlight: 4, limit: 4, queued: 0 }), 0);
+ expect(v.badge).toBe("success");
+ expect(v.busy).toBe(false);
+ });
+
+ it("normalizes garbage counts to 0 and a malformed limit to 1", () => {
+ const v = viewConcurrencyStatus(
+ {
+ providerId: "x",
+ limit: -3,
+ inFlight: Number.NaN,
+ queued: "oops" as unknown as number,
+ paused: false,
+ },
+ 0,
+ );
+ expect(v.limit).toBe(1);
+ expect(v.inFlight).toBe(0);
+ expect(v.queued).toBe(0);
+ expect(v.inFlightLabel).toBe("0/1");
+ });
+
+ it("viewConcurrencyStatuses maps a list preserving order", () => {
+ const views = viewConcurrencyStatuses(
+ [status({ providerId: "a" }), status({ providerId: "b" })],
+ 0,
+ );
+ expect(views.map((v) => v.providerId)).toEqual(["a", "b"]);
+ });
+});
+
+// ── viewConcurrencyLimit ───────────────────────────────────────────────────────
+
+describe("viewConcurrencyLimit", () => {
+ it("passes through id + normalizes the limit", () => {
+ const v = viewConcurrencyLimit({ providerId: "umans", limit: 4 });
+ expect(v.providerId).toBe("umans");
+ expect(v.limit).toBe(4);
+ });
+
+ it("clamps a malformed limit to 1", () => {
+ expect(viewConcurrencyLimit({ providerId: "x", limit: 0 }).limit).toBe(1);
+ expect(viewConcurrencyLimit({ providerId: "x", limit: -2 }).limit).toBe(1);
+ expect(viewConcurrencyLimit({ providerId: "x", limit: 2.9 }).limit).toBe(2);
+ });
+
+ it("viewConcurrencyLimits maps a list preserving order", () => {
+ const views = viewConcurrencyLimits([
+ { providerId: "a", limit: 1 },
+ { providerId: "b", limit: 2 },
+ ]);
+ expect(views.map((v) => v.providerId)).toEqual(["a", "b"]);
+ });
+});
+
+// ── summarizeLimits / summarizeStatus ──────────────────────────────────────────
+
+describe("summarizeLimits", () => {
+ it("empty → No limits configured", () => {
+ expect(summarizeLimits([])).toBe("No limits configured");
+ });
+ it("counts limits (singular/plural)", () => {
+ expect(summarizeLimits([{ providerId: "a", limit: 1 }])).toBe("1 limit configured");
+ expect(
+ summarizeLimits([
+ { providerId: "a", limit: 1 },
+ { providerId: "b", limit: 2 },
+ ]),
+ ).toBe("2 limits configured");
+ });
+});
+
+describe("summarizeStatus", () => {
+ it("empty → No limits configured", () => {
+ expect(summarizeStatus([], 0)).toBe("No limits configured");
+ });
+ it("aggregates providers + in-flight totals", () => {
+ const s = summarizeStatus(
+ [
+ status({ providerId: "a", limit: 4, inFlight: 2 }),
+ status({ providerId: "b", limit: 6, inFlight: 3 }),
+ ],
+ 0,
+ );
+ expect(s).toBe("2 providers · 5/10 in flight");
+ });
+ it("includes queued + paused fragments only when non-zero", () => {
+ const s = summarizeStatus(
+ [
+ status({ providerId: "a", limit: 4, inFlight: 4, queued: 2 }),
+ status({
+ providerId: "b",
+ limit: 4,
+ inFlight: 1,
+ queued: 0,
+ paused: true,
+ pausedUntil: 1000,
+ }),
+ ],
+ 0,
+ );
+ expect(s).toBe("2 providers · 5/8 in flight · 2 queued · 1 paused");
+ });
+ it("singular provider", () => {
+ expect(summarizeStatus([status({ providerId: "a", limit: 4, inFlight: 1 })], 0)).toBe(
+ "1 provider · 1/4 in flight",
+ );
+ });
+});
+
+// ── Network-seam normalizers ───────────────────────────────────────────────────
+
+describe("normalizeConcurrencyLimits", () => {
+ it("coerces a well-formed body", () => {
+ const limits = normalizeConcurrencyLimits({
+ limits: [
+ { providerId: "umans", limit: 4 },
+ { providerId: "openai-compat", limit: 5 },
+ ],
+ });
+ expect(limits).toEqual([
+ { providerId: "umans", limit: 4 },
+ { providerId: "openai-compat", limit: 5 },
+ ]);
+ });
+
+ it("non-array / missing limits → []", () => {
+ expect(normalizeConcurrencyLimits({})).toEqual([]);
+ expect(normalizeConcurrencyLimits({ limits: "nope" })).toEqual([]);
+ expect(normalizeConcurrencyLimits(null)).toEqual([]);
+ expect(normalizeConcurrencyLimits(undefined)).toEqual([]);
+ });
+
+ it("drops entries without a provider id + clamps limits", () => {
+ const limits = normalizeConcurrencyLimits({
+ limits: [
+ { providerId: "umans", limit: 4 },
+ { providerId: "", limit: 9 },
+ { providerId: 123, limit: 1 },
+ { limit: 2 },
+ { providerId: "anthropic", limit: -5 },
+ ],
+ });
+ expect(limits).toEqual([
+ { providerId: "umans", limit: 4 },
+ { providerId: "anthropic", limit: 1 },
+ ]);
+ });
+});
+
+describe("normalizeConcurrencyLimit", () => {
+ it("coerces a well-formed single response", () => {
+ expect(normalizeConcurrencyLimit({ providerId: "umans", limit: 4 })).toEqual({
+ providerId: "umans",
+ limit: 4,
+ });
+ });
+ it("null when the provider id is missing/non-string", () => {
+ expect(normalizeConcurrencyLimit({ limit: 4 })).toBeNull();
+ expect(normalizeConcurrencyLimit({ providerId: "", limit: 4 })).toBeNull();
+ expect(normalizeConcurrencyLimit(null)).toBeNull();
+ });
+ it("clamps a malformed limit to 1", () => {
+ expect(normalizeConcurrencyLimit({ providerId: "x", limit: 0 })).toEqual({
+ providerId: "x",
+ limit: 1,
+ });
+ });
+});
+
+describe("normalizeConcurrencyStatus", () => {
+ it("coerces a well-formed body, preserving pausedUntil only when present", () => {
+ const now = Date.now();
+ const providers = normalizeConcurrencyStatus({
+ providers: [
+ { providerId: "umans", limit: 4, inFlight: 2, queued: 1, paused: false },
+ {
+ providerId: "openai-compat",
+ limit: 5,
+ inFlight: 5,
+ queued: 3,
+ paused: true,
+ pausedUntil: now,
+ },
+ ],
+ });
+ expect(providers).toHaveLength(2);
+ const [first, second] = providers;
+ expect(first).toEqual({
+ providerId: "umans",
+ limit: 4,
+ inFlight: 2,
+ queued: 1,
+ paused: false,
+ });
+ expect(first !== undefined && !("pausedUntil" in first)).toBe(true);
+ expect(second).toEqual({
+ providerId: "openai-compat",
+ limit: 5,
+ inFlight: 5,
+ queued: 3,
+ paused: true,
+ pausedUntil: now,
+ });
+ });
+
+ it("non-array / missing providers → []", () => {
+ expect(normalizeConcurrencyStatus({})).toEqual([]);
+ expect(normalizeConcurrencyStatus({ providers: 42 })).toEqual([]);
+ expect(normalizeConcurrencyStatus(null)).toEqual([]);
+ });
+
+ it("drops entries without a provider id + clamps counts", () => {
+ const providers = normalizeConcurrencyStatus({
+ providers: [
+ { providerId: "umans", limit: 4, inFlight: 2, queued: 1, paused: false },
+ { providerId: "", inFlight: 1 },
+ { limit: 2 },
+ { providerId: 9, inFlight: 0 },
+ { providerId: "x", limit: -1, inFlight: "bad", queued: null, paused: "yes" },
+ ],
+ });
+ expect(providers).toEqual([
+ { providerId: "umans", limit: 4, inFlight: 2, queued: 1, paused: false },
+ { providerId: "x", limit: 1, inFlight: 0, queued: 0, paused: false },
+ ]);
+ });
+
+ it("omits pausedUntil when it is not a finite number", () => {
+ const providers = normalizeConcurrencyStatus({
+ providers: [
+ { providerId: "a", limit: 1, inFlight: 0, queued: 0, paused: true, pausedUntil: "x" },
+ { providerId: "b", limit: 1, inFlight: 0, queued: 0, paused: true, pausedUntil: null },
+ ],
+ });
+ for (const p of providers) expect("pausedUntil" in p).toBe(false);
+ });
+});
diff --git a/src/features/concurrency/logic/view-model.ts b/src/features/concurrency/logic/view-model.ts
new file mode 100644
index 0000000..0a4a7a9
--- /dev/null
+++ b/src/features/concurrency/logic/view-model.ts
@@ -0,0 +1,266 @@
+import type { ConcurrencyStatusEntry } from "@dispatch/transport-contract";
+import type { ConcurrencyLimitEntry } from "./types";
+
+/**
+ * Pure view-models for the concurrency feature — zero DOM, zero effects, zero
+ * Svelte. Maps backend `ConcurrencyLimitEntry` / `ConcurrencyStatusEntry` to
+ * display shapes (badges, "2/4" in-flight labels, pause countdowns, summaries),
+ * holds the limit-input parsing, and the network-seam normalizers the composition
+ * root coerces the untyped JSON with.
+ */
+
+export type Badge = "success" | "warning" | "error" | "neutral";
+
+/** A configured limit row shaped for display. */
+export interface ConcurrencyLimitView {
+ readonly providerId: string;
+ readonly limit: number;
+}
+
+/**
+ * A live status row shaped for display. Carries the raw counts plus pre-computed
+ * labels so the template stays thin.
+ */
+export interface ConcurrencyStatusView {
+ readonly providerId: string;
+ readonly limit: number;
+ readonly inFlight: number;
+ readonly queued: number;
+ readonly paused: boolean;
+ /** "2/4" — in-flight slots held vs the cap. */
+ readonly inFlightLabel: string;
+ /** "1 queued" / "no queue". */
+ readonly queuedLabel: string;
+ /** A pause label when paused, e.g. "paused — resumes in 30s"; null otherwise. */
+ readonly pausedLabel: string | null;
+ readonly badge: Badge;
+ /** True when paused or at capacity (show a spinner). */
+ readonly busy: boolean;
+}
+
+// ── Limit input parsing ───────────────────────────────────────────────────────
+
+/**
+ * Parse a raw limit input into a positive integer, or `null` when it is not a
+ * valid positive integer. Accepts "4" → 4; rejects "0", "-1", "4.5", "", "abc".
+ * Drives the Add/Save button's disabled state so an invalid value never reaches
+ * the backend (the backend is still the authority — it 400s a non-positive body).
+ */
+export function parseLimitInput(value: string): number | null {
+ const trimmed = value.trim();
+ if (trimmed === "" || !/^[0-9]+$/.test(trimmed)) return null;
+ const n = Number.parseInt(trimmed, 10);
+ return Number.isFinite(n) && n >= 1 ? n : null;
+}
+
+/**
+ * Coerce an untrusted limit value into a positive integer (default 1). Used when
+ * normalizing backend responses so a malformed `limit` can never be 0/negative.
+ */
+export function normalizeLimit(value: unknown): number {
+ const n = typeof value === "number" && Number.isFinite(value) ? value : 1;
+ const int = Math.floor(n);
+ return int >= 1 ? int : 1;
+}
+
+// ── Status → display view ──────────────────────────────────────────────────────
+
+const NO_LIMITS = "No limits configured";
+
+/**
+ * Format a remaining-ms delta as a short pause countdown: "30s", "1m 05s",
+ * "resuming" (≤ 0). Pure via the injected `remainingMs`. The component recomputes
+ * this on each status poll (every ~2s) — a 1s ticking timer is optional.
+ */
+export function formatPauseDuration(remainingMs: number): string {
+ if (remainingMs <= 0) return "resuming";
+ const totalSec = Math.floor(remainingMs / 1000);
+ const hours = Math.floor(totalSec / 3600);
+ const mins = Math.floor((totalSec % 3600) / 60);
+ const secs = totalSec % 60;
+ if (hours > 0) return `${hours}h ${String(mins).padStart(2, "0")}m`;
+ if (mins > 0) return `${mins}m ${String(secs).padStart(2, "0")}s`;
+ return `${secs}s`;
+}
+
+/**
+ * The pause label for a status entry, or `null` when not paused. When paused with
+ * a future `pausedUntil`, shows "paused — resumes in 30s"; when paused without a
+ * usable timestamp, shows "paused". Pure via the injectable `now`.
+ */
+export function pauseLabel(
+ paused: boolean,
+ pausedUntil: number | null | undefined,
+ now: number = Date.now(),
+): string | null {
+ if (!paused) return null;
+ if (typeof pausedUntil === "number" && Number.isFinite(pausedUntil)) {
+ const remaining = pausedUntil - now;
+ if (remaining > 0) return `paused — resumes in ${formatPauseDuration(remaining)}`;
+ }
+ // Paused without a usable future timestamp (missing, non-finite, or already
+ // expired): the next status poll will clear `paused`. Show "paused" meanwhile.
+ return "paused";
+}
+
+/**
+ * Build a display view for a status entry. `now` is injectable for tests
+ * (defaults to `Date.now()`); the composition-root component passes nothing in
+ * production (it recomputes on each poll).
+ */
+export function viewConcurrencyStatus(
+ entry: ConcurrencyStatusEntry,
+ now: number = Date.now(),
+): ConcurrencyStatusView {
+ const limit = normalizeLimit(entry.limit);
+ const inFlight = clampCount(entry.inFlight);
+ const queued = clampCount(entry.queued);
+ const paused = entry.paused === true;
+ const atCapacity = inFlight >= limit;
+ let badge: Badge;
+ if (paused) badge = "warning";
+ else if (atCapacity && queued > 0) badge = "warning";
+ else if (inFlight > 0) badge = "success";
+ else badge = "neutral";
+ return {
+ providerId: entry.providerId,
+ limit,
+ inFlight,
+ queued,
+ paused,
+ inFlightLabel: `${inFlight}/${limit}`,
+ queuedLabel: queued === 0 ? "no queue" : `${queued} queued`,
+ pausedLabel: pauseLabel(paused, entry.pausedUntil, now),
+ badge,
+ busy: paused || (atCapacity && queued > 0),
+ };
+}
+
+export function viewConcurrencyStatuses(
+ entries: readonly ConcurrencyStatusEntry[],
+ now: number = Date.now(),
+): readonly ConcurrencyStatusView[] {
+ return entries.map((e) => viewConcurrencyStatus(e, now));
+}
+
+/** A display view for a configured limit entry. */
+export function viewConcurrencyLimit(entry: ConcurrencyLimitEntry): ConcurrencyLimitView {
+ return { providerId: entry.providerId, limit: normalizeLimit(entry.limit) };
+}
+
+export function viewConcurrencyLimits(
+ entries: readonly ConcurrencyLimitEntry[],
+): readonly ConcurrencyLimitView[] {
+ return entries.map(viewConcurrencyLimit);
+}
+
+// ── Summaries ──────────────────────────────────────────────────────────────────
+
+/** A one-line summary of the configured limits list, e.g. "2 limits configured". */
+export function summarizeLimits(limits: readonly ConcurrencyLimitEntry[]): string {
+ if (limits.length === 0) return NO_LIMITS;
+ return `${limits.length} limit${limits.length === 1 ? "" : "s"} configured`;
+}
+
+/**
+ * A one-line summary of the live status, e.g.
+ * "2 providers · 6/10 in flight · 1 queued · 1 paused". Only the queued / paused
+ * fragments appear when non-zero.
+ */
+export function summarizeStatus(
+ providers: readonly ConcurrencyStatusEntry[],
+ now: number = Date.now(),
+): string {
+ if (providers.length === 0) return NO_LIMITS;
+ let inFlight = 0;
+ let limitTotal = 0;
+ let queued = 0;
+ let paused = 0;
+ for (const p of providers) {
+ const limit = normalizeLimit(p.limit);
+ inFlight += clampCount(p.inFlight);
+ limitTotal += limit;
+ queued += clampCount(p.queued);
+ if (p.paused === true) paused += 1;
+ }
+ const parts: string[] = [];
+ parts.push(
+ `${providers.length} provider${providers.length === 1 ? "" : "s"}`,
+ `${inFlight}/${limitTotal} in flight`,
+ );
+ if (queued > 0) parts.push(`${queued} queued`);
+ if (paused > 0) parts.push(`${paused} paused`);
+ // Touch `now` so the summary recomputes alongside the per-row pause countdown.
+ void now;
+ return parts.join(" · ");
+}
+
+// ── Network-seam normalization (pure; called by the composition root) ───────────
+//
+// The concurrency responses are untyped JSON at runtime. The store coerces each
+// defensively HERE (pure + tested) — a malformed/partial backend value (e.g. the
+// extension returning `{}`) can never crash the renderer. Mirrors the
+// `normalizeHeartbeatConfig` / inline `Array.isArray(data.servers)` guards.
+
+function isRecord(value: unknown): value is Record<string, unknown> {
+ return value !== null && typeof value === "object";
+}
+
+function asString(value: unknown): string | null {
+ return typeof value === "string" && value.length > 0 ? value : null;
+}
+
+/** Coerce a non-negative count field to a non-negative integer (0 on garbage). */
+function clampCount(value: unknown): number {
+ const n = typeof value === "number" && Number.isFinite(value) ? value : 0;
+ const int = Math.floor(n);
+ return int >= 0 ? int : 0;
+}
+
+/** Coerce an untrusted `GET /concurrency/limits` body into a typed limit list. */
+export function normalizeConcurrencyLimits(data: unknown): readonly ConcurrencyLimitEntry[] {
+ if (!isRecord(data) || !Array.isArray(data.limits)) return [];
+ const limits = data.limits as readonly unknown[];
+ return limits
+ .filter((r): r is Record<string, unknown> => isRecord(r))
+ .map((r) => ({
+ providerId: asString(r.providerId) ?? "",
+ limit: normalizeLimit(r.limit),
+ }))
+ .filter((r) => r.providerId !== "");
+}
+
+/** Coerce an untrusted `GET`/`PUT /concurrency/limits/:id` body into a limit, or null. */
+export function normalizeConcurrencyLimit(data: unknown): ConcurrencyLimitEntry | null {
+ if (!isRecord(data)) return null;
+ const providerId = asString(data.providerId);
+ if (providerId === null) return null;
+ return { providerId, limit: normalizeLimit(data.limit) };
+}
+
+/**
+ * Coerce an untrusted `GET /concurrency/status` body into a typed status list.
+ * `pausedUntil` is included only when it is a finite number (it is absent when
+ * not paused).
+ */
+export function normalizeConcurrencyStatus(data: unknown): readonly ConcurrencyStatusEntry[] {
+ if (!isRecord(data) || !Array.isArray(data.providers)) return [];
+ const providers = data.providers as readonly unknown[];
+ return providers
+ .filter((r): r is Record<string, unknown> => isRecord(r))
+ .map((r): ConcurrencyStatusEntry => {
+ const base = {
+ providerId: asString(r.providerId) ?? "",
+ limit: normalizeLimit(r.limit),
+ inFlight: clampCount(r.inFlight),
+ queued: clampCount(r.queued),
+ paused: r.paused === true,
+ };
+ const pausedUntil =
+ typeof r.pausedUntil === "number" && Number.isFinite(r.pausedUntil)
+ ? r.pausedUntil
+ : undefined;
+ return pausedUntil !== undefined ? { ...base, pausedUntil } : base;
+ })
+ .filter((r) => r.providerId !== "");
+}