Repository navigation
fix(chat): retry agents that fail to load instead of a dead mode chip - #390
Merged
yuga-hashimoto merged 7 commits intoOct 18, 2026
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
A local OpenCode with plugins takes ~25s on its first GET /agent after a start. During that time the mode chip had no agents and a tap only fired a silent background refresh, so the chip looked dead on every app start or runtime restart. Reproduced on an emulator with a proxy delaying the first /agent by 25s: the first 7 taps (26s) showed nothing. - RuntimeCatalogState.agentsLoading: true while agents are being read and none is on screen (load or the background retry), false once they arrive or the retries give up - the chip swaps its shield for a small spinner while loading - a tap always opens the menu: a disabled "Loading agents…" while loading, "Couldn't load agents. Tap to retry." once retries are over; agents that arrive while it is open replace the placeholder - strings in all 8 locales
- load()'s final write reads the agents inside the update, so a retry that succeeded between its fetch and write is no longer overwritten with an empty list (which also left the spinner running) - a retry tap on a never-connected runtime marks loading synchronously, so repeated taps cannot queue one full reconnect each - the spinner carries the loading content description for TalkBack - zh-CN uses 智能体 like the rest of the locale
… load loading Apply the pre-PR review on the agent retry: - a load whose GET /agent fails but keeps the list already on screen no longer leaves an "Agents: ..." line in the catalogue error, which nothing would clear until the next refresh - a background retry that gives up while a load of the same runtime is still fetching keeps the chip loading instead of offering a retry that would race the load - the chip itself does not ask for a refresh while it is loading
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.
Summary
When a runtime's
GET /agentfailed or answered with nothing selectable (typically a local OpenCode installing its plugins on the first read after a start), that one answer was final until something else refreshed the catalogue. The mode chip kept showingbuildover an empty menu, and tapping it did nothing.RuntimeCatalogRepository.refreshAgents()re-reads the agents in the background (4 retries, 3 s apart) until the runtime offers one to pick. The rest of the catalogue, and every other refresh, no longer waits on a slowGET /agent.refresh()instead of polling a dead server.Agents: ...line is left in the catalogue error.Agents: ...line is removed from the catalogue error. Decoding errors are collapsed to one line so this removal can match them. The load writes its agents inside the same state update it reads them in, so a retry that succeeds between the load's fetch and its write is not overwritten with an empty list.chat_agents_loadingandchat_agents_load_failedare added in all 8 locales.Testing
./gradlew spotlessCheck detekt :app:testGithubDebugUnitTest(JDK 17 + SDK 35): passed.RuntimeCatalogRepositoryTestgets 6 new cases:Last commit
f66bb973applies three of the first review's suggestions:The report below is the re-review of that HEAD.
Pre-PR review
判定: APPROVE
ブロッカー
なし
提案(非ブロッキング)
app/src/test/java/com/yugahashimoto/andcode/data/repository/RuntimeCatalogRepositoryTest.kt — f66bb97 で追加された次の2つの挙動に、直接のテストがありません。現状のコードで正しく動くことはコードを追って確認済みなので、ブロッカーにはしていません。
agentsLoading = it.isRefreshingになり、同じランタイムの load がまだ動いている間は loading を保つ(RuntimeCatalogRepository.kt:360)。RuntimeCatalogRepository.kt:343-344)。今回は見送りと伺っています。どちらも、
agentAnswersに suspend する応答を入れ、runCurrent()で途中の状態を確認すれば再現できます。後続の PR で追加することを勧めます。チェック済み項目
/tmp/opencode/review-upstream-agent-retry.diff、7 コミット: 21b8a75〜f66bb973)を読み直し、追加分(.followup.diff)と worktree の HEAD 版ファイル(RuntimeCatalogRepository.kt:258-365)を突き合わせました。差分とファイルの中身は一致しています。RuntimeCatalogRepository.kt:275-277で、agentsErrorと同じ条件のときはfailures()からreadFailureを除いています。messages.sessions/providers/workspacesで接頭辞が違うので、誤って消えることはありません。readFailure == nullのときも何も除かれません。a refresh whose agent read fails keeps the agents already on screenは、初回 load でbuildを表示し、次の refresh で agents の取得を全部失敗させます。この経路でちょうど該当コードを通るので、agentsErrorとerrorがともに null になるアサーションは有効です。it.isRefreshingを残す形に直っています。agentsLoading = !agentsFoundで必ず上書きされます。接続失敗時は新しいRuntimeCatalogState(loading は false)で書かれます。ランタイムを切り替えたときはcollectLatestが状態をリセットします。refreshAgents()を呼ぶ時点では、前のリトライのジョブは既に終わっているので(isActive == false)、新しいリトライが正しく起動します。ChatHomeScreen.kt:1908がselectableAgents.isEmpty() && !loadingになり、メニュー項目のenabled = !loadingと揃っています。chat_agents_loadingとchat_agents_load_failedがあることを確認しました。追加コミットで文字列の変更はありません。Kotlin にハードコードされた表示文字列もありません。agentsLoadingはSettingsViewModel.kt:222→AndCodeApp.kt:1065→ChatHomeScreen→ChatComposer→ModeChipと渡っています。onRefreshAgentsはAndCodeApp.kt:1133で渡しています。ChatVoiceInstrumentedTest.kt:34とプレビュー(ChatHomeScreen.kt:2235)はそのままコンパイルできます。AppModule.kt:205)とAndCodeApplication.kt:507に影響はありません。agentRetryはsynchronized(agentRetryLock)内で参照・起動されます。loadAgentsは毎回選択中のランタイムを確認するので、切り替え後は自然に終了します。load()内のreturnは inline のwithLockを抜ける non-local return で、接続失敗時や切り替え後は末尾のrefreshAgents()に到達しません。listAgentsは静的な一覧かPermissionModeChip経由なので、今回のリトライ処理には入らないことを確認しました。.worktree/配下にあり、上流 PR なので upstream/main 基点であることは依頼で明示されています。scripts/new-worktree.shで作ったかどうかまでは検証できていません。