Repository navigation
Description
Symptom
- User types a message; nothing happens anywhere — no reply, and (per user report) nothing server-side either.
- Tapping the agent chip (plan → build) appears to do nothing.
User environment: multiple project directories in use, several custom agents defined in opencode config.
Root causes
1. Catalog/session directory-scoping mismatch → Agent not found before save
catalog.load() always fetches /agent, /command, /provider through the active connection's client (fixed x-opencode-directory header), but prompts are sent via a session-directory-scoped client (clientFor(session.directory)). For any session whose project differs from the connection default, the selected agent/model may not exist on the instance that receives the prompt.
Verified against upstream packages/opencode/src/session/prompt.ts (createUserMessage): an unresolvable agent name throws before the user message is saved — the server never records anything. The only signal back is a session.error SSE event.
2. Async send failures are never rendered
events.ts routes session.error into useSessions.error, but app/session/[id].tsx never renders that error state — the user sees their optimistic bubble and silence.
3. Agent cycling silently no-ops
cycleAgent() returns without feedback when fewer than 2 primary agents are loaded (e.g. /agent failing under the active directory scope, or stale Directory field on the connection). The filter requires exact "primary"|"all" — agents returned without mode produce an empty cycle list. Upstream #21364/#22130 additionally pollute the primary list with config-defined subagents (mode defaults to all).
4. SSE startup serialized behind metadata fetches
Same family as #189: _layout.tsx starts SSE + catalog only after loadConnections() commits client, which awaits project.current() + path.get() (up to 30s timeout each).
Fix plan (approved)
- Per-directory catalog scoping:
catalog.load(directory?)usesclientForDirectory(directory); per-directory selection cache;[id].tsxpasses the session's directory. - Surface async failures: render
useSessions.erroras a dismissible banner above the composer (+ i18n). - cycleAgent robustness: filter
(a.mode ?? "all") !== "subagent", feedback when tap is a no-op. - Decouple SSE startup: commit client first, fetch metadata fire-and-forget with stale-guard (port of fork fix fix: keyboard/composer (#156), auto-scroll (#155), dead SSE (#186), blocked SSE startup (#189), stale transcripts, uncopyable text, axis-perfect table scrolling, 50-session list cap, transcript flash, dark-mode contrast #182).
Diagnostics (run when on the server's network)
# /agent listing under each scope — compare modes/names:
Invoke-RestMethod "http://<server>:4096/agent"
Invoke-RestMethod "http://<server>:4096/agent" -Headers @{ "x-opencode-directory" = "<project-B>" }
# Discriminating send test on a throwaway session, then watch the TUI:
# POST /session/{id}/prompt_async body {"parts":[{"type":"text","text":"ping"}]}
# repeat with "agent":"<custom-agent>" → expect Agent-not-found error pathDev server 100.108.64.76:4096 was unreachable from the build machine during this task (infra) — diagnostics pending.
Activity
involvex commentedon Aug 26, 2026
Implemented (all four fixes)
Step 1 — Per-directory catalog scoping (
src/stores/catalog.ts,app/session/[id].tsx)catalog.load(directory?)now routes/agent,/command,/providerthroughclientForDirectory(directory); the session screen passes its session's own directory, so the agent/model chips always describe the same server instance prompts are sent to.Step 2 — Async failures visible (
app/session/[id].tsx, i18n)useSessions.error("Message failed: …"), covering server-side rejections that arrive via SSE afterprompt_asyncalready returned 204 (e.g.Agent not found, model/provider errors).session.banners.sendError,common.dismiss(en + zh-Hans).Step 3 — Agent cycling robustness (
src/lib/agent-selection.tsnew,catalog.ts, chip UI)(mode ?? "all") !== "subagent"— matches the server schema default and survives upstream's config-mode-dropping bug (#21364/#22130); missingmodeno longer empties the cycle list.cycleAgentreturns whether it cycled; a no-op tap now alerts "No other primary agents available" instead of feeling dead.Step 4 — SSE startup decoupled from metadata (
src/stores/connections.ts){client, clientBase}immediately, then fillcurrentProject/serverHomefire-and-forget behind an active-connection stale-guard. Live events/catalog can no longer be delayed by up to 2×30s of hanging metadata requests (ports fork fix fix: keyboard/composer (#156), auto-scroll (#155), dead SSE (#186), blocked SSE startup (#189), stale transcripts, uncopyable text, axis-perfect table scrolling, 50-session list cap, transcript flash, dark-mode contrast #182, closes SSE event stream startup is blocked behind /project/current and /path metadata fetches #189).Also fixed two pre-existing lint errors surfaced along the way: hook-deps warning in
[id].tsxand the control-char regex inheaders.ts.Verification
bun run typecheckbun run lintbun run testsrc/lib/agent-selection.test.ts)The mandatory CUA showcase could not run here: no emulator/device attached, no Azure OpenAI env file, and dev server
100.108.64.76:4096unreachable from this Windows box (CUA infra lives on the Mac host per AGENTS.md). Do not merge until the showcase passes on the Mac or via CI:Suggested manual matrix when a server is reachable:
fix: scope agent catalog per directory and surface async send errors
docs: split plan.md into NOW (v2 fix) vs LATER priority tables
feat: OpenCode v2 server support (v0.4.16)