v2.2-paseo-providers: Paseo provider stack + v2.2.1 pane-scoped chat fixes

Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch
rewrite with streaming/persist, permission prompts, and agent commands.
Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline,
WS user-delta replace, and inference orphan tool_call stripping.
Archive openspec v2-2; update CHANGELOG and CURRENT.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-26 15:18:31 +00:00
parent 04673eaf59
commit 93d3f86c2b
96 changed files with 6694 additions and 1329 deletions

122
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,122 @@
# BooCode architecture
Last updated: 2026-05-25. **Navigation:** `AGENTS.md`. **Deep reference:** `CLAUDE.md`.
## System overview
```mermaid
flowchart TB
subgraph client [Browser]
SPA["apps/web React SPA"]
end
subgraph edge [Edge]
Caddy --> Authelia
end
subgraph host ["Host 100.114.205.53"]
subgraph docker [Docker boocode_net]
BooChat["boocode container<br/>apps/server + built web<br/>:9500"]
BooTerm["booterm container<br/>apps/booterm<br/>:9501"]
PG[("boocode_db<br/>Postgres 16<br/>database: boochat<br/>host :5500")]
CC["codecontext sidecar<br/>:8080 internal"]
end
BooCoder["boocoder.service<br/>apps/coder<br/>:9502"]
Agents["Host CLI agents<br/>opencode goose claude qwen"]
LLM["llama-swap<br/>100.101.41.16:8401"]
end
Authelia --> SPA
SPA -->|"HTTP /api WS /api/ws"| BooChat
SPA -->|"WS /ws/term"| BooTerm
SPA -->|"HTTP /api/coder proxy<br/>WS direct"| BooCoder
BooChat --> PG
BooTerm --> PG
BooCoder --> PG
BooChat -->|"HTTP tools"| CC
BooChat -->|"streamText"| LLM
BooCoder -->|"native inference"| LLM
BooCoder -->|"ACP or PTY spawn"| Agents
Agents --> LLM
```
## Three surfaces, one database
| Surface | Code | Runtime | Primary role |
|---------|------|---------|--------------|
| BooChat | `apps/server` + `apps/web` | Docker | Read-only chat, file tools, MCP client, skills |
| BooTerm | `apps/booterm` + terminal panes in `apps/web` | Docker | tmux + xterm.js PTY panes |
| BooCoder | `apps/coder` + `CoderPane` in `apps/web` | Host systemd | Write tools, task queue, ACP/PTY agent dispatch |
All surfaces share Postgres (`boochat` DB). Cross-surface joins link chats, tasks, and sessions.
## BooChat request path
```mermaid
sequenceDiagram
participant U as User
participant W as apps/web
participant S as apps/server
participant DB as Postgres
participant L as llama-swap
U->>W: POST message
W->>S: /api/sessions/:id/messages
S->>DB: user + assistant streaming rows
S->>S: inference.enqueue()
loop outer step loop
S->>L: streamText
L-->>S: deltas / tool calls
S->>W: WS frames via broker
opt tool calls
S->>S: executeToolPhase
S->>DB: message_parts
end
end
S->>DB: finalize message
S->>W: session_updated user frame
```
Key modules: `services/inference/turn.ts` (outer loop), `stream-phase.ts` (AI SDK adapter), `tool-phase.ts`, `services/broker.ts`, `hooks/useSessionStream.ts`.
## BooCoder execution paths
```mermaid
flowchart LR
Msg["User message<br/>CoderPane"] --> Route{"provider?"}
Route -->|boocode| Inf["In-process inference<br/>pending_changes queue"]
Route -->|external| Task["tasks row<br/>dispatcher poll"]
Task --> ACP["ACP dispatch<br/>opencode goose"]
Task --> PTY["PTY dispatch<br/>claude qwen"]
ACP --> Host["spawn install_path<br/>on host"]
PTY --> Host
Inf --> Apply["apply_pending → disk"]
```
Since v2.1.0, BooCoder runs on the host (not Docker). Agent binaries spawn directly — no SSH tunnel.
## Supporting services
| Service | Reachability | Purpose |
|---------|--------------|---------|
| codecontext | `http://codecontext:8080` from Docker network | Code graph / symbol analysis (Go sidecar) |
| llama-swap | `LLAMA_SWAP_URL` env | Local LLM inference + model props |
| SearXNG | `SEARXNG_URL` (Tailscale Fathom) | `web_search` / `web_fetch` when enabled |
| MCP servers | `/data/mcp.json` config | Optional tools (e.g. Context7), read-only in BooChat |
## Config and data files
| Path | Role |
|------|------|
| `data/AGENTS.md` | Global agent registry (bind-mounted `/data/AGENTS.md`) |
| `data/mcp.json` | MCP server config (opencode-compatible shape) |
| `data/skills/` | On-demand skill library |
| `BOOCHAT.md` / `BOOCODER.md` | Container guidance (mtime-cached into system prompt) |
| `apps/server/src/schema.sql` | Canonical DB schema |
## Deploy topology
- **BooChat + BooTerm + Postgres + codecontext:** `docker compose up --build -d` from `/opt/boocode`
- **BooCoder:** `pnpm -C apps/server build && pnpm -C apps/coder build && sudo systemctl restart boocoder`
- **Ports bind to Tailscale IP** `100.114.205.53`, not `0.0.0.0` — use that IP for host smoke curls

312
docs/DEFERRED-WORK.md Normal file
View File

@@ -0,0 +1,312 @@
# Deferred work — post stale cleanup (2026-05-26)
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
---
## Summary
| 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~~ | ~~Medium~~ | **RESOLVED — v2.3 provider lifecycle** |
| Unified `packages/types` | Maintainability | Low (dev-only) | MediumHigh | 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 |
---
## 1. Task cancel → abort external ACP/PTY child
### Current behavior
External agent tasks (opencode, cursor, claude, qwen, goose, etc.) flow through `apps/coder/src/services/dispatcher.ts` path B:
1. Create git worktree (`worktrees.ts` → local `hostExec`)
2. Spawn ACP or PTY child with an **`AbortController`** (`ac`) passed as `signal`
3. Stream assistant output over session WS
4. Diff worktree → `pending_changes`
5. Cleanup worktree
The abort signal is **created per task** but **never registered anywhere cancel can reach**:
```typescript
// dispatcher.ts — ac is local to dispatchExternal(); no Map<taskId, AbortController>
const ac = new AbortController();
// ...
await dispatchViaAcp({ ..., signal: ac.signal });
// or
await dispatchViaPty({ ..., signal: ac.signal });
```
Cancel paths today:
| Route | What it does | Gap |
|-------|--------------|-----|
| `POST /api/tasks/:id/cancel` | Sets DB `state = 'cancelled'`, calls `cancelPendingPermission`, tries `inference.cancel()` on session chats | **`inference.cancel` only affects native boocode streaming** — not ACP/PTY children |
| `POST /api/sessions/:sessionId/stop` | Same — loops open chats, calls `inference.cancel` | Same gap for external tasks tied to that session |
| Dispatcher `stop()` on shutdown | Sets `stopping = true`; in-flight external task may finish or mark cancelled at end | No immediate child kill |
`dispatchViaAcp` and `dispatchViaPty` **do** honor `signal` when aborted (worktree ops, stream read, spawn teardown). The wiring from HTTP cancel → `ac.abort()` is missing.
There is also **no frontend** calling task cancel today (`grep` across `apps/web` finds no `cancelTask` usage). Stop in CoderPane targets native inference only.
### Symptoms users would see
- Click Stop / cancel task → DB says `cancelled` but agent process keeps running on host
- Assistant row may stay `streaming` until the child exits naturally
- Worktree under `/tmp/booworktrees/<taskId>` may linger until dispatcher cleanup or manual removal
- Permission prompts cancel correctly (`cancelPendingPermission`) — that part works
### Proposed implementation
**Phase A — backend registry (minimum viable)**
1. Add `Map<taskId, AbortController>` (or `{ ac, childPid? }`) in dispatcher module scope
2. On `dispatchExternal` start: `activeAbort.set(taskId, ac)`
3. On task completion/failure/cleanup: `activeAbort.delete(taskId)`
4. Export `cancelExternalTask(taskId: string): boolean` from dispatcher
5. In `POST /api/tasks/:id/cancel`:
- Call `cancelExternalTask(taskId)` **before** or **instead of** `inference.cancel` when task is external (`execution_path` or agent !== boocode)
- Mark assistant message `cancelled` / `failed` and publish `message_complete` + `chat_status: idle` on session WS
6. Wire `POST /api/sessions/:sessionId/stop` to find **running external tasks** for that session and abort them too
**Phase B — child process kill**
- PTY: store `child` ref from `spawn`; on abort, `child.kill('SIGTERM')` then SIGKILL after timeout
- ACP: `acp-dispatch` already uses signal; ensure `createAcpNdJsonStream` cancel kills the underlying spawn (verify in `acp-stream.ts`)
- Worktree: on abort mid-flight, call `cleanupWorktree` in `finally` block
**Phase C — frontend**
- CoderPane Stop button: if `provider !== 'boocode'` and a `task_id` is active, call `POST /api/coder/tasks/:id/cancel` (needs API client method)
- Show cancelled state in UI (task row or composer status)
### Files likely touched
- `apps/coder/src/services/dispatcher.ts` — registry, export cancel
- `apps/coder/src/routes/tasks.ts` — call dispatcher cancel
- `apps/coder/src/routes/messages.ts` — session stop for external tasks
- `apps/coder/src/services/acp-dispatch.ts`, `pty-dispatch.ts` — verify signal → kill
- `apps/web/src/api/client.ts`, `CoderPane.tsx` — Stop wiring
### Acceptance criteria
- Cancel a running opencode/cursor task → child process gone within 5s (`ps` / `pgrep` on host)
- Task row `state = 'cancelled'`, assistant message not left `streaming`
- Worktree directory removed (best-effort)
- Native boocode cancel unchanged
- No regression on permission-denied / blocked flows
### Open questions
- Should cancel be **best-effort** (mark cancelled even if kill fails) or **fail closed** until process dies?
- Arena parallel tasks: cancel one contestant vs whole arena?
- Blocked task waiting on permission: cancel already resolves waiter — confirm ACP session teardown order
---
## 2. ~~Skip ACP cold probe when DB models are fresh~~ **RESOLVED — v2.3 provider lifecycle**
Addressed in [`openspec/changes/v2-3-provider-lifecycle/`](../openspec/changes/v2-3-provider-lifecycle/design.md). The v2.3 snapshot module (`provider-snapshot.ts`) uses DB `available_agents` models as the warm path and only cold-probes on explicit `POST /api/providers/refresh`. Opening the provider picker no longer triggers any probe. `PROVIDER_PROBE_TTL_MS` env var (default 24h) controls stale-model self-heal.
---
## 3. Unified `packages/types` for provider snapshot JSON
### Current behavior
Provider snapshot shapes are **duplicated** (not byte-identical exports):
| Location | Types |
|----------|-------|
| `apps/coder/src/services/provider-types.ts` | `ProviderSnapshotEntry`, `AgentCommand`, `ProviderModel`, … |
| `apps/web/src/api/types.ts` | Same names, hand-maintained |
| WS frames | `agent_commands` frame in `ws-frames.ts` (server + web copy) |
Today drift is caught by:
- TypeScript at compile time when fields are added to one side only
- `provider-snapshot.test.ts` on coder
- Manual review during v2.2 batch
There is **no** `packages/` workspace yet (monorepo is `apps/server`, `apps/web`, `apps/coder`, `apps/booterm` only).
### Options
**A. Zod schema + inferred types (lighter)**
- Add `ProviderSnapshotEntrySchema` in server or coder
- Web imports schema for runtime validation on fetch (optional)
- Parity test: `expect(webTypeKeys).toEqual(zodShapeKeys)` — similar to existing `ws-frames.test.ts`
**B. Shared `packages/types` package**
```
packages/types/
package.json # @boocode/types
src/provider.ts # interfaces + zod
src/ws-frames.ts # move duplicated unions here
```
- `apps/coder`, `apps/web`, `apps/server` depend on `workspace:*`
- Requires NodeNext export map, build order (`tsc` for types package first)
- Biggest payoff if WS frames + provider + task types all move
**C. Status quo + discipline**
- Keep duplicated TS interfaces
- Add one test file importing both sides and asserting structural equality
### Tradeoffs
| Approach | Setup cost | Runtime safety | Best when |
|----------|------------|----------------|-----------|
| A — Zod in coder | Low | Validate API responses | Snapshot is coder-only API |
| B — packages/types | High | Full shared source | Many cross-app types growing |
| C — parity test | Lowest | Compile-time only | Rare schema changes |
### Recommendation
Start with **A or C** unless planning a broader “shared types” initiative (tasks, permissions, arena). Full `packages/types` is justified when a third consumer appears or WS frame duplication becomes painful again.
### Files likely touched (option B)
- New `packages/types/`
- Root `pnpm-workspace.yaml`
- `apps/web/tsconfig`, `apps/coder/tsconfig` — path references
- Strip duplicate blocks from `apps/web/src/api/types.ts`
---
## 4. Large file splits (refactor when touched)
Not user-facing defects — deferred to avoid scope creep in the stale batch. Split when the next feature touches the file heavily.
### 4.1 `CoderPane.tsx` (~663 lines)
**Responsibilities today (single component):**
- Session WS + polling fallback for messages
- Pending changes fetch/apply/discard
- External vs native send paths (`AgentComposerBar`, slash commands, skill invoke)
- Permission card + agent commands hint
- Provider snapshotdriven composer state
**Suggested extractions:**
| Hook / module | Owns |
|---------------|------|
| `useCoderMessages(sessionId)` | WS subscribe, poll-when-disconnected, temp-id dedup |
| `usePendingChanges(sessionId)` | List, apply, discard, diff preview trigger |
| `CoderComposerFooter` | AgentComposerBar + hints + permission card layout |
**Trigger to do it:** next CoderPane feature (e.g. task cancel UI, multi-task inbox) or when file exceeds ~800 lines.
### 4.2 `ChatInput.tsx` (~669 lines)
**Responsibilities:** attachments, drag/drop, paste-as-file, `@` mentions, slash picker, agent picker row, context bar, send/submit, registry for send-to-chat.
**Suggested extractions:**
| Hook / module | Owns |
|---------------|------|
| `useSlashCommandInput(...)` | `slashState`, `isSlashCommandToken`, picker open/close |
| `useFileMention(...)` | `@` detection, file index fetch, popover state |
| `useAttachments(...)` | chips, drop, paste, MAX_ATTACHMENTS |
Slash helpers already live in `lib/slash-command.ts`; hooks would consume them.
### 4.3 `dispatcher.ts` (~483 lines)
**Two paths in one file:**
- Path A: native boocode — enqueue inference, wait for message completion
- Path B: external — worktree, ACP/PTY, diff, pending_changes
**Suggested split:**
- `dispatcher-native.ts` — poll loop pick-up for `provider = boocode` or no agent
- `dispatcher-external.ts` — path B + abort registry (pairs with item §1)
- `dispatcher.ts` — thin poll loop + routing
### 4.4 `AgentComposerBar.tsx` (~323 lines)
Optional split: **provider/model/mode/thinking pickers** vs **persistence** (`AgentSessionConfig` localStorage/session). Lower priority — file is manageable.
### 4.5 `acp-dispatch.ts` (~294 lines)
Stream setup already extracted to `acp-stream.ts`. Remaining split candidate: **session lifecycle** (create → prompt loop → teardown) vs **permission/tool side effects**.
### General rule
Split only when:
1. A new feature needs a clear seam, or
2. Review feedback repeatedly hits the same file, or
3. Tests need isolated units (e.g. dispatcher external path)
Avoid drive-by splits — they churn blame without shipping user value.
---
## 5. Retire `apps/coder/web/` fallback SPA
### What it is
Standalone Vite React app (`@boocode/coder-web`) built into `apps/coder/web/dist/` and served by BooCoder Fastify at `:9502` when no BooChat proxy is in front.
**Primary UI:** `CoderPane` inside BooChat SPA (`apps/web`) — API via `/api/coder/*` proxy, WS to `:9502`.
**Fallback UI:** direct visit to `http://100.114.205.53:9502` — own `api/client.ts`, `ChatPane`, `DiffPane`, duplicated types.
### Why it was kept
- Host-only debugging without full BooChat stack
- Historical path before CoderPane integration
- Zero dependency on Authelia-protected BooChat origin
### Cost of keeping
- Separate build step in coder deploy
- Duplicate message/stream types and API paths
- Features land in CoderPane first; fallback rots unless manually updated
### Options
| Option | When to choose |
|--------|----------------|
| **Keep** | Still use `:9502` directly for debugging or demos |
| **Freeze** | Stop feature work; build only on release for emergency access |
| **Remove** | Always use BooChat; `:9502` serves health + WS + API only (or minimal static “open in BooChat” page) |
### Removal checklist (if chosen)
1. Confirm Sam never uses standalone UI (bookmarks, systemd docs, BOOCODER.md)
2. Remove `apps/coder/web/` package and static serve from `apps/coder/src/index.ts`
3. Update Dockerfile/coder build scripts
4. Keep WS + REST routes — CoderPane depends on them
5. Optional: single-page static “Use code.indifferentketchup.com” at `/`
---
## Suggested batch ordering
If picking these up as openspec batches:
1. **Task cancel abort** — highest correctness gap; unblocks honest Stop button in CoderPane
2. **ACP probe skip** — quick win for provider picker latency once semantics agreed
3. **CoderPane hook extraction** — natural follow-on when adding cancel UI
4. **Zod parity or packages/types** — when next WS/provider field is added
5. **Retire coder/web** — only after explicit “I dont use :9502 UI” confirmation
---
## Related docs
- [`STALE-DEPRECATED.md`](./STALE-DEPRECATED.md) — resolved stale items
- [`ARCHITECTURE.md`](./ARCHITECTURE.md) — BooChat / BooCoder surfaces
- [`openspec/changes/v2-2-paseo-providers/design.md`](../openspec/changes/v2-2-paseo-providers/design.md) — provider snapshot API
- [`BOOCODER.md`](../BOOCODER.md) — dispatch, worktrees, pending changes

77
docs/STALE-DEPRECATED.md Normal file
View File

@@ -0,0 +1,77 @@
# Stale / deprecated inventory
Review list for Sam — **do not delete blindly**. Items marked "candidate remove" have no known runtime imports; items marked "deprecated" are still referenced or documented.
Last updated: 2026-05-26 (stale cleanup batch)
---
## Resolved in stale cleanup (2026-05-26)
| Item | Resolution |
|------|------------|
| `ProviderPicker.tsx` | **Removed** — replaced by `AgentComposerBar` |
| `SkillSlashCommand.tsx` | **Removed** — inlined `SlashCommandPicker` in `ChatInput` |
| `Provider` type + `api.coder.providers()` | **Removed** |
| `GET /api/providers` (flat list) | **Removed** — snapshot is canonical |
| `ssh.ts` + worktree SSH | **Removed**`worktrees.ts` uses local `host-exec.ts` |
| `ProviderCommand` vs `AgentCommand` | **Consolidated**`AgentCommand` in `provider-types.ts` |
| `pty-dispatch` `AgentCommand` | **Renamed**`PtySpawnSpec` / `buildPtySpawnSpec` |
| `ProviderSnapshotStatus: 'unavailable'` | **Dropped** — only `'ready'` \| `'error'` |
| Skill invoke duplication | **Extracted**`@boocode/server/skill-invoke` |
| `resolveChatId` duplication | **Extracted**`apps/coder/src/routes/chat-resolve.ts` |
| Qwen settings parse | **Shared**`qwen-settings.ts` with `readFile` |
| ChatInput slash regex | **Shared**`lib/slash-command.ts` |
---
## Frontend (apps/web)
_No open stale items from the 2026-05-26 review._
---
## BooCoder backend (apps/coder)
| Item | Status | Notes |
|------|--------|-------|
| [`apps/coder/web/`](../apps/coder/web/) | **Fallback SPA** | Standalone BooCoder UI at `:9502`. Primary UI is `CoderPane` in BooChat SPA. Keep unless host-only access is dropped. |
---
## Large files (refactor when touched, not delete)
| File | ~Lines | Split suggestion |
|------|--------|------------------|
| [`CoderPane.tsx`](../apps/web/src/components/panes/CoderPane.tsx) | 660+ | Extract `useCoderMessages`, pending changes hook, composer footer |
| [`ChatInput.tsx`](../apps/web/src/components/ChatInput.tsx) | 670+ | Extract slash/mention hooks |
| [`dispatcher.ts`](../apps/coder/src/services/dispatcher.ts) | 480+ | Split native vs external paths |
| [`AgentComposerBar.tsx`](../apps/web/src/components/AgentComposerBar.tsx) | 320+ | Optional: prefs vs picker UI |
| [`acp-dispatch.ts`](../apps/coder/src/services/acp-dispatch.ts) | 300+ | Stream setup now in `acp-stream.ts`; session lifecycle could split further |
---
## Docs / plans superseded by v2.2
| Item | Notes |
|------|-------|
| [`docs/superpowers/plans/2026-05-25-provider-picker-backend.md`](../docs/superpowers/plans/2026-05-25-provider-picker-backend.md) | Pre-v2.2 flat `/api/providers` plan; header marks superseded. |
| Roadmap / CHANGELOG mentions of `ProviderPicker` | Historical truth for v2.1.0 — leave as release record. |
---
## Already cleaned in simplify pass (2026-05-26)
- Extracted [`acp-stream.ts`](../apps/coder/src/services/acp-stream.ts) (shared ACP NDJSON bridge)
- Deduped `mergeCommands` → [`provider-commands.ts`](../apps/coder/src/services/provider-commands.ts)
- Parallel ACP probes + singleflight in [`provider-snapshot.ts`](../apps/coder/src/services/provider-snapshot.ts)
- Removed dead `getPendingSessionId` from [`permission-waiter.ts`](../apps/coder/src/services/permission-waiter.ts)
- CoderPane: WS message dedup, slash helpers, poll only when WS disconnected
---
## Skipped (needs product decision)
- **Task cancel → abort external ACP/PTY child** — `AbortController` in dispatcher not wired to cancel route
- **Skip ACP cold probe when DB models fresh** — perf; changes snapshot semantics
- **Unified `packages/types`** for provider snapshot JSON — Zod parity test may suffice

View File

@@ -1,12 +1,14 @@
# BooCoder Provider Picker — Backend (Steps 13)
> **Superseded:** Shipped as `v2.1.0-provider-picker` (2026-05-25). Agent discovery uses direct `exec()` on the host, not SSH. See `CHANGELOG.md` and `apps/coder/src/services/agent-probe.ts` for current implementation.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Expose a `GET /api/providers` endpoint on BooCoder (port 9502) that returns all available providers with their model lists, so the frontend can build a two-level provider → model picker.
**Architecture:** A static provider registry maps agent names to their metadata (transport, model source). The existing `agent-probe.ts` is extended to discover models for each agent and persist them in a new `models` JSONB column on `available_agents`. A new `/api/providers` route merges the registry with DB state and llama-swap models to produce the response.
**Tech Stack:** Fastify, postgres (porsager), Zod, SSH exec to host for agent discovery.
**Tech Stack:** Fastify, postgres (porsager), Zod, direct host exec for agent discovery (historical plan referenced SSH — superseded at v2.1.0).
---