Skip to content

Messages silently lost across projects + agent cycling dead: catalog/session directory mismatch, invisible async errors #198

Closed

Description

@involvex

Symptom

  1. User types a message; nothing happens anywhere — no reply, and (per user report) nothing server-side either.
  2. 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)

  1. Per-directory catalog scoping: catalog.load(directory?) uses clientForDirectory(directory); per-directory selection cache; [id].tsx passes the session's directory.
  2. Surface async failures: render useSessions.error as a dismissible banner above the composer (+ i18n).
  3. cycleAgent robustness: filter (a.mode ?? "all") !== "subagent", feedback when tap is a no-op.
  4. 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 path

Dev server 100.108.64.76:4096 was unreachable from the build machine during this task (infra) — diagnostics pending.

Activity

  1. involvex commented on Aug 26, 2026

    @involvex
    Author

    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, /provider through clientForDirectory(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.
    • Selections (agent/model/variant) are remembered per directory scope — switching projects restores that project's last-used agent instead of leaking one project's custom agents into another.
    • Legacy call sites (tab screen, app bootstrap) keep the active-connection scope.

    Step 2 — Async failures visible (app/session/[id].tsx, i18n)

    • Red dismissible banner above the composer renders useSessions.error ("Message failed: …"), covering server-side rejections that arrive via SSE after prompt_async already returned 204 (e.g. Agent not found, model/provider errors).
    • Keys added: session.banners.sendError, common.dismiss (en + zh-Hans).

    Step 3 — Agent cycling robustness (src/lib/agent-selection.ts new, catalog.ts, chip UI)

    • Primary filter is now (mode ?? "all") !== "subagent" — matches the server schema default and survives upstream's config-mode-dropping bug (#21364/#22130); missing mode no longer empties the cycle list.
    • cycleAgent returns 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)

    Also fixed two pre-existing lint errors surfaced along the way: hook-deps warning in [id].tsx and the control-char regex in headers.ts.

    Verification

    Gate Result
    bun run typecheck ✅ clean
    bun run lint ✅ clean
    bun run test ✅ 288/288 (9 new tests in src/lib/agent-selection.test.ts)

    ⚠️ CUA smoke — BLOCKED on this machine (infra)

    The mandatory CUA showcase could not run here: no emulator/device attached, no Azure OpenAI env file, and dev server 100.108.64.76:4096 unreachable 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:

    python3 scripts/android-cua-smoke.py --model gpt-5.4 --include-xml

    Suggested manual matrix when a server is reachable:

    1. Cross-project session + custom agent selected → message appears in TUI (previously silently dropped).
    2. Force a rejection (rename the selected custom agent server-side) → red banner shows the reason.
    3. Tap agent chip plan↔build with only built-ins → switches; with subagents present → skips them.
  2. added a commit that references this issue on Nov 4, 2026
    66ad0da
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions