Repository navigation
fix(chat): retry agents that fail to load instead of a dead mode chip - #13
Merged
Merged
Conversation
A local OpenCode with plugins (oh-my-opencode-slim, opencode-mem, ...) installs them on its first GET /agent after a start: on a fresh cache that call took ~25s here, and it can fail outright. The catalogue took that one answer as final, so the agent list stayed empty until something else refreshed it, and the mode chip kept showing "build" over a menu with nothing in it - tapping it did nothing visible. Reproduced on an emulator against a proxy that fails the first /agent: the chip stayed dead for the whole session, across 60s and a background/foreground round trip. - RuntimeCatalogRepository retries GET /agent (4 x 3s) while the answer is a failure or an empty list - refreshAgents() re-reads only the agents, keeping the rest of the catalogue - the mode chip asks for a refresh when tapped with no agents, instead of opening an empty menu
Per review: the agent retry ran inside load()'s refresh lock, holding the whole catalogue (and every other refresh) behind up to several slow GET /agent calls. load() now reads agents once and publishes, then a single background retry (agentRetryJob) runs outside the lock. - a failed or empty agent read no longer blanks agents this runtime already showed (same rule as providers, yuga-hashimoto#373) - repeated taps on an empty chip share one retry instead of stacking polls - a runtime that never connected gets a full refresh() from the chip - success means at least one selectable primary agent, matching the chip
Per review: a first GET /agent failure put "Agents: ..." into the catalogue error, and the background retry only replaced the list, so the workspaces screen kept showing the failure after the agents had arrived. The agents line is kept separately and dropped when the retry fills the list. Also: a retry left over from a runtime the user switched away from is cancelled rather than blocking the new runtime's, and starting a retry is synchronized since taps (main) and load (IO) both reach it.
- decoding errors carry the JSON input on a second line, so the agents failure never matched a line of the joined error and stayed on screen after a successful retry; it is flattened to one line when recorded - a chip tap while the runtime is still connecting no longer starts its own agent poll ahead of the load, which retries the agents once connected
This was referenced Oct 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes "the build (agent mode) chip does nothing when tapped" on dp4, with a local OpenCode running oh-my-opencode-slim.
Root cause. A local OpenCode with plugins (oh-my-opencode-slim, opencode-mem, ...) installs them on its first
GET /agentafter a start. With an empty plugin cache that call took ~25s against OpenCode 1.18.5 and the user's real config, and it can fail. The catalogue took that single answer as final, so the agent list stayed empty and the mode chip kept showing the remembered "build" over an empty menu: tapping it opened nothing visible.#9 (
hidden: null) is in dp4 and correct. dp4 decodes the real/agentoutput fine; the read itself never succeeded.Reproduced on an emulator. I put a proxy in front of a real server that fails the first
/agent. On dp4 the chip stayed dead for the whole session (60s, background/foreground round trip), and/agentwas never asked again.Fix
load()reads agents once and publishes the catalogue. If no agent is selectable, a single background retry (refreshAgents, 4 × 3s) runs outside the refresh lock, so sessions, providers and other refreshes never wait on a slow/agent.refresh(); one still connecting is left to its load.Verified
./gradlew detekt spotlessCheck :app:testGithubDebugUnitTest :app:assembleGithubDebug, 1405 unit tests passing (6 new inRuntimeCatalogRepositoryTest)./agentfails: the menu (orchestrator/build/plan) is there within 9s, and no "Agents:" error is left on screen.plansticks.Follow-ups
Pre-PR review
Reviewed by repo-reviewer in three rounds, each APPROVE, at 06dbac0, 4d680dd and 69a1d3b. Each round's non-blocking suggestions were applied in the next commit. The last commit, 8a6b766, applies round 3's two remaining small suggestions: one-line agents error, and no early poll while connecting. It is verified locally (1405 tests) and was not sent for another round. Round 3's report:
判定: APPROVE
ブロッカー
なし
提案(非ブロッキング)
RuntimeCatalogRepository.kt:392:複数行のエラーメッセージだと、エラー行が消えずに残ります。withAgents()はerrorを行ごとに分け、agentsErrorと完全に一致する行だけを取り除きます。agentsErrorが複数行だと、どの行とも一致しないので何も消えません。formatHttpError(OpenCodeApiClient.kt:712-718)がスニペットを" "でつないで 1 行にしているからです。"...\nJSON input: ..."のように複数行になることがあります。fix(opencode): decode agents whose hidden flag is null #9 で直したhidden: nullも、まさにこの種類の失敗でした。errorを組み立て直す。または、記録するときにagentsErrorの改行を空白に置き換える。RuntimeCatalogRepository.kt:304-308:接続確認中(health == null && isRefreshing)にタップすると、refresh()には切り替わらず、再試行 job が起動します。/agentを問い合わせ続けます。load()の最後のrefreshAgents()は、同じランタイムの job が動いているので何もしません。そのため実際に再試行するのはこの早すぎた job だけで、これが接続完了より先に失敗し終えると自動回復は起きません(タップすれば回復します)。health == nullのときはisRefreshingかどうかにかかわらず再試行を起動せず、実行中のload()に任せる(isRefreshingのときは何もせず return)。:310-315)。チェック済み項目
RuntimeCatalogRepository.ktの該当箇所(30-89、160-394 行)とテスト(274-353 行)を直接読みました。gradle とエミュレータでの確認結果は、報告されたものをそのまま受け取っています。agentsErrorの流れを確認しました。load()が最後に書き込むとき、毎回新しい値を設定します(:268)。接続失敗やランタイム切替で state を作り直すときは null に戻ります(:87/179/202)。refresh()している間に古い job が成功しても、withAgentsが消すのはその時点のagentsErrorです。他のエラー(sessions/providers/workspaces)は残ります。RuntimeCatalogState(...)を生成しているのはこのファイルの中だけで、新しいフィールドにはデフォルト値があります。外部の呼び出し元への影響はありません。failures()はagentsFailure()を共有しているので、errorの中の行とagentsErrorの文字列が必ず一致します。synchronizedは inline なので、:313のreturnは non-local return として正しく動きます。scope.launchはブロックしないので、ロックを持ったまま suspend することはありません。delayかlistAgentsの途中で止まり、書き込みには届きません。届いた場合も:320-321の id 確認で弾かれます。refreshAgents → refresh()に進むのはhealth == null && !isRefreshingのときだけです。load()の最後のrefreshAgents()(:273)に届くのは接続に成功した後だけで、そのときhealthは null ではありません。an agent retry for a runtime that never connected…(:301)は、/agentの呼び出し回数が増えないことと、エラーが"offline"であることを確かめています。既存テスト(:297)では、再試行の成功後にerror == nullを確認するようになっています。:304は約 133 文字なので範囲内です。新しいユーザー向け文字列は無く、i18n への影響もありません。DI のコンストラクタも変わっていません。