diff options
| author | Adam Malczewski <[email protected]> | 2026-06-28 13:18:25 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-28 14:41:47 +0900 |
| commit | 0f1b04bd7976ea416f11d83e4b7b78cf537bfdf3 (patch) | |
| tree | b198da2719987ffd58e9c2633fc36684cf2ad316 /.dispatch | |
| parent | a59200e786f7d97d7ba5b9cd2bee9ffef531dac2 (diff) | |
| download | dispatch-web-0f1b04bd7976ea416f11d83e4b7b78cf537bfdf3.tar.gz dispatch-web-0f1b04bd7976ea416f11d83e4b7b78cf537bfdf3.zip | |
feat(concurrency): configurable cooldown + adaptive-headroom auto-reduce banner
Consumes the backend's concurrency-fixes (commit 2d27666) — additive to
[email protected], NO version bump. ConcurrencyStatusEntry gains
cooldownMs / autoReduced / autoReducedFrom? / notice?; new
ConcurrencyCooldownResponse / SetConcurrencyCooldownRequest + GET/PUT
/concurrency/cooldown/:providerId.
FE:
- Pure core (logic/view-model.ts): DEFAULT_COOLDOWN_MS; parseCooldownInput
(non-negative int — 0 is valid, unlike the limit); normalizeCooldown;
cooldownLabel ("350ms"/"1.2s"/"0ms (off)"); viewConcurrencyStatus extended
(cooldown + autoReduce fields; auto-reduce → warning badge, not busy);
viewAutoReduce/autoReduceNotices (banner view — prefers backend notice,
synthesizes a fallback); summarizeStatus "N auto-reduced" fragment;
normalizeConcurrencyStatus coerces new fields (immutable readonly build;
autoReducedFrom/notice only when autoReduced===true); normalizeConcurrencyCooldown.
- Types (logic/types.ts): re-export the 2 new contract types +
ConcurrencyCooldownResult + Get/SaveConcurrencyCooldown ports.
- UI: ConcurrencyCooldownRow.svelte (inline-edit cooldown + Save → PUT, seeded
via the ChatLimitField pattern); AutoReduceBanner.svelte (dismissible banner —
backend notice + "Was N, now M." + "Restore to N"); ConcurrencyView renders
the cooldown per status card + the banner section. The banner persists while
autoReduced===true, is dismissible, and clears automatically once a poll shows
autoReduced===false after a restore PUT.
- Store (store.svelte.ts): getConcurrencyCooldown + setConcurrencyCooldown
(surface 400/404/503 as ok:false; normalize at the seam) + interface decls.
- App.svelte: saveConcurrencyCooldown adapter → ConcurrencyView.
- Tests: +47 (view-model cooldown/auto-reduce/normalizers; component banner
render, cooldown PUT, restore clears banner, dismiss; store cooldown GET/PUT).
Re-synced the file: dep (bun install) + re-mirrored .dispatch/transport-contract.
reference.md. typecheck 0/0, 1048 tests green (run twice), biome clean, build OK.
Worktree env: an untracked dispatch-backend → backend symlink was created in the
worktree parent so the canonical file:../dispatch-backend/... paths resolve
(NOT committed — per the §2d/§2j worktree convention).
Diffstat (limited to '.dispatch')
| -rw-r--r-- | .dispatch/transport-contract.reference.md | 53 |
1 files changed, 53 insertions, 0 deletions
diff --git a/.dispatch/transport-contract.reference.md b/.dispatch/transport-contract.reference.md index 6a5a6b3..0582d94 100644 --- a/.dispatch/transport-contract.reference.md +++ b/.dispatch/transport-contract.reference.md @@ -8,6 +8,21 @@ > **Orchestrator:** SNAPSHOT of `[email protected]` (MCP status + computers + provider concurrency). Regenerate whenever > it changes. > +> **2026-06-27 update (concurrency-fixes — ADDITIVE, NO version bump):** the provider concurrency surface gains +> (a) a configurable + persisted per-provider release COOLDOWN, and (b) adaptive headroom. `ConcurrencyStatusEntry` +> gains FOUR new fields: `cooldownMs: number` (REQUIRED — per-slot release cooldown in ms, default 350; a recycled slot is +> held this long before the next waiter is admitted), `autoReduced: boolean` (REQUIRED — true when the limit was auto-reduced +> by 1 after a 429, one-way + persisted; the FE renders a visible banner), `autoReducedFrom?: number` (present only when +> `autoReduced===true` — the original limit before reduction), and `notice?: string` (present only when `autoReduced===true` — +> a human-readable banner message). The banner is DISMISSIBLE / persists while `autoReduced===true`; it clears when the user +> restores the limit via `PUT /concurrency/limits/:providerId` (a manual PUT clears `autoReduced` server-side). NEW cooldown +> endpoints: `GET /concurrency/cooldown/:providerId` → `ConcurrencyCooldownResponse` (`{ providerId, cooldownMs }`) — 404 when +> the provider has no concurrency config at all (no limit, no cooldown), 503 when the extension isn't loaded; +> `PUT /concurrency/cooldown/:providerId` ← `SetConcurrencyCooldownRequest` (`{ cooldownMs }` — must be a non-negative integer, +> 0 = no cooldown / instant re-admission) → `ConcurrencyCooldownResponse` — 400 on an invalid body, 503 when not loaded. +> Persists + applies immediately to subsequently recycled slots. Also (backend-only, no FE surface): a usage gate polls upstream +> `concurrent_sessions` before admitting a queued agent. See `backend-handoff.md` §2j-update-3. +> > **2026-06-26 delta (provider concurrency — `[email protected]` bump):** adds the > per-provider concurrency-limits API types: `ConcurrencyLimitsResponse` (`GET /concurrency/limits`), > `SetConcurrencyLimitRequest` + `ConcurrencyLimitResponse` (`GET`/`PUT /concurrency/limits/:providerId`), @@ -1062,6 +1077,17 @@ export interface ConcurrencyLimitResponse { * - `queued`: how many agents are waiting for a slot. * - `paused`: whether the queue is paused due to a 429 backoff. * - `pausedUntil`: when the pause expires (epoch-ms), present only when paused. + * - `cooldownMs`: the per-slot release cooldown (ms). A recycled slot is held + * this long before the next waiter is admitted — covers the upstream + * provider's accounting lag. Configurable + persisted per provider. + * - `autoReduced`: whether the limit was auto-reduced by 1 after a 429 + * (adaptive headroom, one-way, persisted). The user restores the limit + * manually via `PUT /concurrency/limits/:providerId`, which clears the flag. + * When `true`, the frontend renders a visible notice/banner. + * - `autoReducedFrom`: the original limit before auto-reduction (present only + * when `autoReduced` is true). + * - `notice`: a human-readable notice string for the frontend to render as a + * banner when the limit was auto-reduced (present only when `autoReduced`). */ export interface ConcurrencyStatusEntry { readonly providerId: string; @@ -1070,6 +1096,10 @@ export interface ConcurrencyStatusEntry { readonly queued: number; readonly paused: boolean; readonly pausedUntil?: number; + readonly cooldownMs: number; + readonly autoReduced: boolean; + readonly autoReducedFrom?: number; + readonly notice?: string; } /** * Response of `GET /concurrency/status` — live status for every provider with a @@ -1078,5 +1108,28 @@ export interface ConcurrencyStatusEntry { export interface ConcurrencyStatusResponse { readonly providers: readonly ConcurrencyStatusEntry[]; } + +// ─── Provider concurrency cooldown ──────────────────────────────────────────── + +/** + * Response of `GET /concurrency/cooldown/:providerId` — the per-slot release + * cooldown (ms) for a provider. A recycled slot is held this long before the + * next waiter is admitted, covering the upstream provider's accounting lag. + * When no cooldown was explicitly set, the server default (350ms) is returned. + */ +export interface ConcurrencyCooldownResponse { + readonly providerId: string; + readonly cooldownMs: number; +} + +/** + * Body of `PUT /concurrency/cooldown/:providerId` — set the release cooldown + * (ms) for a provider. `cooldownMs` must be a non-negative integer (0 = no + * cooldown, instant re-admission). The value is persisted and applied to + * subsequently recycled slots. + */ +export interface SetConcurrencyCooldownRequest { + readonly cooldownMs: number; +} ``` |
