From 6d03690a65dc8f63322b40109be3e527c55aead6 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Fri, 29 May 2026 20:20:31 +0000 Subject: [PATCH] docs: v2.3 provider-lifecycle closeout (Phase 6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BOOCODER.md gains a Provider lifecycle section (config file + schema, gitignored-with-exception, the 24h PROVIDER_PROBE_TTL_MS refresh contract, enable/disable via Settings → Providers, custom-ACP add, native boocode always-on, the honest subset-refresh known limitation, deploy + smoke). docs/DEFERRED-WORK.md §2 (cold-probe skip) marked ADDRESSED with the still- deferred Tier-2 follow-ups listed. CHANGELOG gets the v2.5.13 batch-closeout entry. Docs only — no code. Co-Authored-By: Claude Opus 4.8 (1M context) --- BOOCODER.md | 78 +++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 4 +++ docs/DEFERRED-WORK.md | 21 ++++++++---- 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/BOOCODER.md b/BOOCODER.md index 96432a7..960e094 100644 --- a/BOOCODER.md +++ b/BOOCODER.md @@ -37,3 +37,81 @@ Every file modification queues in `pending_changes` before touching disk. The us - Never count `dist/` directory sizes as source lines. Only count `src/**/*.ts` files. Compiled output is inflated by inlined types and transpilation artifacts. - Before claiming a feature works, run the actual command and show the output. "Should work" is not verification. Acceptable evidence: test output (`pnpm test`), build output (`pnpm build`), curl response, docker logs, `\d tablename` output. If you can't run it, say so explicitly — don't assert success without evidence. - When reporting counts (tools, tests, files, routes, lines), derive the number from a command (`grep -c`, `wc -l`, test runner output) — not from memory or approximation. + +## Provider lifecycle (v2.3) + +BooCoder's coding agents are a **config-backed registry**: built-ins live in `provider-registry.ts`, and `data/coder-providers.json` layers overrides + custom entries on top. Registration ≠ installation — the config lists what you *want*; a probe reports what's *ready*. + +### Config file: `data/coder-providers.json` + +Resolved from `CODER_PROVIDERS_PATH` (default `/data/coder-providers.json`; dev/host path `/opt/boocode/data/coder-providers.json`). It is **tracked in git** via a `.gitignore` exception (the rest of `data/*` is ignored). A missing file, invalid JSON, or a schema mismatch all fall back to built-ins-only — loading never throws at startup. + +```json +{ + "providers": { + "goose": { "enabled": false }, + "amp-acp": { + "extends": "acp", + "label": "Amp", + "description": "ACP wrapper for Amp", + "command": ["amp-acp"], + "enabled": true + } + } +} +``` + +Per-provider override fields (all optional): + +| Field | Meaning | +|-------|---------| +| `extends` | `"acp"` — required for a NEW (custom) provider; built-in overrides omit it | +| `label` | Display name (required for custom) | +| `description` | Sub-label shown in the picker / settings | +| `command` | `[binary, ...args]` to spawn (required for custom; overrides a built-in's default argv) | +| `env` | Extra env vars merged into the spawn | +| `enabled` | Default `true`; `false` hides it from the composer | +| `order` | UI sort key | +| `models` / `additionalModels` | Replace / merge onto the discovered model list | + +A PATCH to one provider id **replaces that id's override object wholesale** (per-id shallow merge), so to flip a single field keep the rest; a `null` value for an id deletes its override (reverts to the built-in default). + +### Refresh contract + +The snapshot is cached and a provider's cold ACP probe (tier-2) is **skipped** while `available_agents.last_probed_at` is younger than `PROVIDER_PROBE_TTL_MS` (default `86400000` = 24h). Opening the composer is therefore fast and does not re-probe. To force a cold re-probe (after installing a CLI or editing models): **`POST /api/providers/refresh`** (the Refresh button in the Providers settings tab), which clears the cache and re-probes. + +### Enable / disable + +Two ways: +- **Settings → Providers tab** — open the sidebar → **Settings** → **Providers**: toggle a provider on/off, refresh it, or open its diagnostic. (Earlier builds exposed a gear in the composer; that control was moved into Settings.) +- **Edit the config** (`"enabled": false`) then `POST /api/providers/refresh`. + +A **disabled** provider leaves the composer's provider picker but stays listed in the Providers tab (status "Disabled") so you can re-enable it. **Native `boocode` is always-on** — an `enabled:false` on it is ignored (with a warn log) and it is never rendered as toggleable. + +### Adding a custom ACP provider + +- **Catalog modal**: Providers tab → **Add provider** → pick an entry → it PATCHes the config (`extends:'acp'` + label + command, enabled) and refreshes that provider. +- **Hand-edit** `data/coder-providers.json`: add an id with `extends:'acp'`, `label`, and `command`, then `POST /api/providers/refresh`. + +Either way, **adding to config does NOT install the binary.** Until the CLI is on `PATH` the provider shows **"Not installed"** (status `unavailable`) and does not appear in the composer picker. + +### Known limitation — subset refresh + +`POST /api/providers/refresh` accepts an optional `{ "providers": ["id", ...] }` body and returns a `refreshed` count scoped to that subset — **but the underlying cold re-probe currently covers ALL installed providers**, not just the requested subset. True per-provider force is a future change (it needs a snapshot-internal parameter). This is intentional for now, not a bug: a subset refresh still re-probes everything; only the reported count is scoped. + +### Deploy + smoke + +Two deploy targets: +- **Routes (host service):** `pnpm -C apps/server build && pnpm -C apps/coder build && sudo systemctl restart boocoder` +- **Web UI (container):** `docker compose up --build -d boocode` + +Green gate (verified across phases 1–5): `pnpm -C apps/coder test` (134 passing) `&& pnpm -C apps/coder build`. + +Smoke (via Tailscale): + +```bash +curl http://100.114.205.53:9502/api/providers/snapshot # lists every registered provider +curl http://100.114.205.53:9500/api/coder/providers/config # raw config, through the BooChat proxy +# Settings → Providers: disable goose → it leaves the composer picker, stays in the tab +# POST refresh → models repopulate; Add a catalog entry → it appears after refresh (unavailable until its CLI is installed) +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index cc47932..fd8ac50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes per release tag. Most recent on top, ordered by tag creation date (which matches the git history). Tag names follow `vMAJOR.MINOR.PATCH-slug` — the slug describes what shipped, so the tag name alone is enough to recall the batch. +## v2.5.13-provider-lifecycle-phase5 — 2026-05-29 + +Closeout of the v2.3 provider-lifecycle batch — the web UI (Phase 5) plus docs (Phase 6). Provider management moved into **Settings → Providers**: a tab listing every registered provider with a status badge (Available / Disabled / Not installed / Error / Loading), an enable/disable toggle, a per-provider refresh, and a plaintext diagnostic; toggling sends the provider's *full* override (preserving a custom ACP entry's command under the wholesale-replace PATCH merge) then refetches the snapshot. The composer's provider picker now filters to `enabled && (status === 'ready' || 'loading')`, so disabled and unavailable providers drop out of the picker and are managed only in settings (native `boocode` always shows). A curated ACP catalog (`apps/web/src/data/acp-provider-catalog.ts`) + `AddProviderModal` register custom providers via `PATCH /api/providers/config` then a subset refresh, and the web client gained `getProvidersConfig` / `patchProvidersConfig` / `refreshProviders` / `getProviderDiagnostic`. Two mobile fixes ship alongside: the Settings pane is now reachable on phones (opening it pushes `?pane=` atomically so the mobile URL-sync effect keeps it active instead of snapping back to the chat pane), and the Add-provider modal caps to the viewport with a single `overscroll-contain` scroll region so the list scrolls instead of dragging the whole modal. This completes the arc begun in `v2.5.4-provider-lifecycle-phase1` (config-backed registry over the built-ins) → `v2.5.5-provider-lifecycle-phase2` (loading/unavailable snapshot lifecycle + tier-2 probe TTL gate) → `v2.5.6-provider-lifecycle-phase3` (generic `resolveLaunchSpec` ACP dispatch) → `v2.5.12-provider-lifecycle-phase4` (config GET/PATCH, subset refresh, diagnostic HTTP API). Docs landed in `BOOCODER.md` (config file, refresh contract, enable/disable, custom ACP, the honest subset-refresh known limitation) and `docs/DEFERRED-WORK.md` §2 is marked addressed; the remaining Tier-2 follow-ups (WS `provider_snapshot_updated` frame, `available_agents.enabled` column, shared types package, MCP provider tools) stay deferred. + ## v2.5.12-provider-lifecycle-phase4 — 2026-05-29 Phase 4 of the v2.3 provider-lifecycle batch (`openspec/changes/v2-3-provider-lifecycle/design.md` §6): the HTTP API to read, patch, refresh, and diagnose providers. `routes/providers.ts` gains `GET /api/providers/config` (the raw loaded `CoderProvidersFile`), `PATCH /api/providers/config` (a partial providers map — an id's override object is replaced wholesale, a `null` value deletes it), an optional `{ providers?: string[] }` body on `POST /api/providers/refresh` (the `refreshed` count reflects the requested subset; the force probe itself still covers all installed providers, since per-provider force is a snapshot-internal change left to a later phase), and `GET /api/providers/:id/diagnostic` returning JSON `{ diagnostic: string }` — a read-only report (resolved def, install_path, last_probed_at, enabled, `which` availability, last cached probe error) with no probe spawn. PATCH correctness is the whole story: the order is validate→save→reload→clear, a malformed body or an invalid merged config returns 422 without writing the file, and a `save()` failure returns 500 without reloading the registry or clearing the snapshot cache, so on-disk and in-memory state can never diverge. New pure `mergeProviderConfigPatch` + `ProviderConfigPatchSchema` in `provider-config.ts`, a read-only `peekSnapshotEntry` cache accessor (source of the diagnostic's last-error — no probe/cache logic change), and a new `provider-diagnostic.ts` formatter. The web client gains `api.coder.getProvidersConfig` / `patchProvidersConfig` / `refreshProviders(providers?)` / `getProviderDiagnostic`, with mirrored `ProviderOverride` / `CoderProvidersFile` / `ProviderConfigPatch` types; the existing `/api/coder/*` proxy blanket-forwards the new routes with no change. +28 tests (134 coder total: pure merge/validate, the diagnostic formatter, and `app.inject` route tests proving the 422-no-write and save-fail-no-divergence guards). The diagnostic returns JSON rather than the §8 plaintext so it flows through the JSON `request` client helper (reconciling design §6.4's `{ diagnostic }` with §8's string report). No UI (Phase 5). Builds on `v2.5.6-provider-lifecycle-phase3`. diff --git a/docs/DEFERRED-WORK.md b/docs/DEFERRED-WORK.md index 34cbdc8..b549cf9 100644 --- a/docs/DEFERRED-WORK.md +++ b/docs/DEFERRED-WORK.md @@ -2,7 +2,7 @@ This document describes work intentionally **not** shipped in the 2026-05-26 stale/simplify batch. Each item needs a product or architecture decision before implementation. See also [`STALE-DEPRECATED.md`](./STALE-DEPRECATED.md) for what was resolved in that batch. -Last updated: 2026-05-26 +Last updated: 2026-05-29 --- @@ -11,7 +11,7 @@ Last updated: 2026-05-26 | Item | Category | User impact | Effort | Risk if left alone | |------|----------|-------------|--------|-------------------| | Task cancel → abort ACP/PTY child | Correctness / UX | High — Stop does not kill external agents | Medium | Zombie processes, stuck `running` tasks, orphaned worktrees | -| Skip ACP cold probe when DB fresh | Performance | Medium — composer open can stall 5–30s on cache miss | Medium (v2.3 batch) | Slow provider picker; repeated ACP spawns on every snapshot rebuild | +| Skip ACP cold probe when DB fresh | Performance | Medium — composer open can stall 5–30s on cache miss | ✅ Shipped (v2.3, Phase 2) | Resolved — `PROVIDER_PROBE_TTL_MS` TTL gate live | | Unified `packages/types` | Maintainability | Low (dev-only) | Medium–High | Type drift between server, coder, web | | Large file splits | Maintainability | None directly | Medium per file | Harder reviews, merge conflicts | | Retire `apps/coder/web/` fallback SPA | Scope / ops | Low — Sam uses CoderPane | Medium | Dual UI maintenance, divergent API client | @@ -111,7 +111,7 @@ There is also **no frontend** calling task cancel today (`grep` across `apps/web ## 2. Skip ACP cold probe when DB models are fresh -**Status:** Planned — [`openspec/changes/v2-3-provider-lifecycle/`](../openspec/changes/v2-3-provider-lifecycle/proposal.md). **Not shipped** (no `v2.3` tag; all tasks unchecked). +**Status:** ✅ **ADDRESSED** in v2.3 (phases 1–5: `v2.5.4-provider-lifecycle-phase1` … `v2.5.12-provider-lifecycle-phase4`, plus the phase-5 settings UI + picker filter). The `PROVIDER_PROBE_TTL_MS` (default 24h) gate on `available_agents.last_probed_at` is live — the tier-2 cold ACP probe runs only on `force` (`POST /api/providers/refresh`), TTL staleness, or empty DB models; otherwise the snapshot serves cached models. See [`openspec/changes/v2-3-provider-lifecycle/`](../openspec/changes/v2-3-provider-lifecycle/proposal.md). The original (v2.2) behavior below is kept for history. ### Current behavior (v2.2) @@ -140,12 +140,21 @@ See [`design.md`](../openspec/changes/v2-3-provider-lifecycle/design.md): v2.2 shipped the snapshot wire shape and ACP dispatch stack. Lifecycle semantics (config registry, enable/disable, probe TTL, settings UI) were scoped as the follow-on **v2.3** batch to avoid mixing two large behavior changes in one tag. -### Acceptance criteria (when v2.3 ships) +### Acceptance criteria — met -- Second `GET /api/providers/snapshot` within TTL does not invoke `probeAcpProvider` (mock assert in tests) -- Disabled provider visible in settings, absent from composer +- Second `GET /api/providers/snapshot` within TTL does not invoke `probeAcpProvider` (mock assert in `provider-snapshot.test.ts`) +- Disabled provider visible in settings (Providers tab), absent from composer - Explicit refresh repopulates models; warm open is sub-second +### Still deferred (Tier-2 follow-ups, not shipped in v2.3) + +These were explicitly scoped out of v2.3 (see `design.md` §11) and remain open: + +- **`provider_snapshot_updated` WS frame** — the loading state uses a capped client poll / one-shot refetch instead of a server-pushed frame (design §4.4, §11; tasks O.1). +- **`available_agents.enabled` DB column** — `enabled` is read from the in-memory resolved registry only; no DB mirror, so settings state after a coder restart re-derives from the JSON config rather than the DB (design §3.3; tasks O.2). +- **Single-source-of-truth shared types package** — the provider snapshot types are duplicated across `apps/coder/.../provider-types.ts` and `apps/web/src/api/types.ts`, guarded by the text-identity `provider-types-parity.test.ts` rather than a shared package (see §3 below). +- **MCP `list_providers` / `inspect_provider` tools** — provider introspection over MCP is not wired (design §11). + --- ## 3. Unified `packages/types` for provider snapshot JSON