Repository navigation
feat(codex): choose reasoning effort and fast mode per turn - #389
Merged
yuga-hashimoto merged 22 commits intoOct 20, 2026
Merged
yuga-hashimoto merged 22 commits into
yuga-hashimoto merged 22 commits into
Conversation
Providers reject file parts outside images/PDF (for example `'file part media type application/zip' functionality not supported`), so a zip or other binary attachment could not be sent at all. When the composer attaches a MIME a runtime cannot carry as a file part, copy it into the workspace under `.andcode-attachments/` and reference the path in the prompt text instead. Images and PDFs keep going as file parts. This only applies to the on-device runtime, whose `/workspace` is a local mount (`BackendKind.LOCAL`); a remote runtime is unaffected and still receives the attachment directly, since there is no local mount to copy into. PromptAttachment gains a `workspacePath`; ChatViewModel partitions attachments into model parts and workspace references so the on-disk copy is never sent. The `.andcode-attachments` directory name is now shared with Antigravity's inline-attachment path.
…s on opt-in A runtime/provider session failure was reported to Crashlytics as the raw provider message wrapped in an exception. That text is free-form - it can quote prompts, model output, file paths and hosts - and Crashlytics shares its collection switch with Analytics, which was opt-in. A user who never enabled analytics still had crash reports collected in release builds. - Report a session failure as a fixed category only: `Runtime session error: <runtime>/<errorType>`, where errorType is accepted only if identifier-shaped (ProviderAuthError, ...) and otherwise becomes `unknown`. The raw message stays on-device in the notification. - Disable Crashlytics collection by default in the manifest (firebase_crashlytics_collection_enabled=false) and follow the analytics/crash opt-in via CrashReporter.setEnabled(); debug builds stay off regardless. - Turn off advertising-ID collection and remove the AD_ID permission. - Broaden SecretRedaction: the credential behind a Basic Authorization header, every Cookie pair, quoted JSON credential fields, env-style key names (ANTHROPIC_API_KEY, refresh_token) and URL userinfo are now redacted. - Refresh the bundled legal docs (EN and JA) to match.
With oh-my-opencode-slim installed, OpenCode's GET /agent reports the plugin agents (orchestrator, fixer, explorer, ...) with "hidden": null. OpenCodeAgent.hidden was a non-null Boolean, so kotlinx.serialization rejected the whole list, the catalogue fell back to no agents, and the mode chip opened an empty menu - tapping "build" appeared to do nothing. Make hidden nullable (V1 and V2 DTOs) and treat only true as hidden. Verified against a live OpenCode 1.19.3 server with the plugin enabled.
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
Codex's model/list reports each model's supported reasoning efforts and service tiers, but the app only read id and name, and turn/start carried neither setting, so the thinking chip never appeared for Codex and fast mode could not be turned on. - CodexModels fills variants from supportedReasoningEfforts (server order) and records the fast tier, its description, and each model's default effort and service tier - every turn sends explicit effort and serviceTier: the app-server applies them to subsequent turns too, so an omitted value would silently keep the previous turn's (resolveCodexTurnSettings) - a global fast-mode choice (null = follow each model's default) is persisted and exposed to the chat as fastModeAvailable/Enabled - composer gets a Fast chip next to the thinking chip; labels for the none/minimal/ultra efforts in all locales - verified against codex 0.163.0 generated app-server types and a live model/list response
- 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
…386-387 fix: keep runtime error text on device and copy unsupported attachments into the workspace
…gent-hidden-null fix(opencode): decode agents whose hidden flag is null
…gent-retry fix(chat): retry agents that fail to load instead of a dead mode chip
Termux dropped proot 5.1.107.96 from packages.termux.dev (404 on every mirror it was on; grimler.se answers with a body that does not match the pinned SHA-256). A CI run that misses the generated-runtime cache then fails in :app:prepareOpenCodeRuntimeAssets before any test starts, as the re-run of the yuga-hashimoto#388 merge commit did twice. - Refresh runtime_tools/termux_assets.lock.json with `prepare_android_runtime_assets.py --lock-only`. Only proot changes (5.1.107.96 -> 5.1.107.97, new filename and SHA-256 for arm64-v8a and x86_64); libandroid-shmem and libtalloc are unchanged. - Mirror the matching termux-packages recipe: only TERMUX_PKG_VERSION and TERMUX_PKG_SHA256 differ from the previous one. - Point THIRD_PARTY_NOTICES.md and its offline copy at the 5.1.107.97 source archive and SHA-256. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xw2ucui6t6dQdyGRrnCbHb
The mirror is now at proot 5.1.107.97 while the fdroid pin still builds 5.1.107.94, so "a version or two" no longer describes the gap. Applies a non-blocking suggestion of the pre-PR review, in both the root document and its offline copy. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xw2ucui6t6dQdyGRrnCbHb
…d-xnh8mt fix(build): pin proot 5.1.107.97, the version Termux still serves
yuga-hashimoto
pushed a commit
that referenced
this pull request
Oct 18, 2026
Conflicts with #391 (workspace attachments) in ChatViewModel and ChatHomeScreen, resolved by keeping both sides: - ChatViewModel constructor: keep workspaceAttachmentNoteTemplate and the Codex fast-mode parameters. - Prompt request: send the Codex service tier together with modelAttachments. #391 split pendingAttachments into modelAttachments and workspaceAttachments, and #389 still passed pendingAttachments to the request; pendingAttachments stays for the optimistic message and previews. - ChatHomeScreen imports: keep Role and the semantics imports. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xw2ucui6t6dQdyGRrnCbHb
Owner
|
@copilot この pull request のマージの競合を修正してください。 |
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
Codex's
model/listreports each model's supported reasoning efforts and service tiers, but the app only readidandname, andturn/startcarried neither setting. So the thinking chip never appeared for Codex, and fast mode could not be turned on.CodexModelsturnssupportedReasoningEffortsinto model variants, in the server's order, so the existing thinking chip lists them.none,minimalandultraget labels in all locales. An effort the app does not know yet shows its raw id, capitalized.serviceTierslists the "Fast" tier (priority), or that uses the deprecatedadditionalSpeedTiers: ["fast"], gets a bolt Fast chip beside the thinking chip. The tier's own server description is the chip's screen-reader text. The user's choice is global and persisted. Unset means each model follows its owndefaultServiceTier.effortandserviceTierexplicitly (resolveCodexTurnSettings). The app-server applies both to subsequent turns as well, so an omitted value would silently keep the previous turn's.model_reasoning_effortorservice_tierset in~/.codex/config.toml, because the app now always says what the chips show.PromptRequest.serviceTieris Codex-only. The OpenCode V1 and V2 clients build their request bodies field by field, so it never reaches an OpenCode server.docs/CODEX.mddocuments both controls.Testing
./gradlew spotlessCheck detekt :app:lintGithubDebug :app:testGithubDebugUnitTest(JDK 17 + SDK 35): passed.CodexModelsTest: efforts, fast tier, defaults, and the legacy speed tier, from realmodel/listshapes.CodexTurnStartParamsTest: theturn/startpayload.CodexServiceTierTest.ChatViewModelTest: UI state and what is sent.model/listresponse.58947621applies three of the reviewer's non-blocking suggestions below:docs/CODEX.mdno longer claims 0.163.0 is the first version with these fields.fastModeDescriptionKDoc now matches its use.chatViewModel::setFastModereplaces the lambda.Pre-PR review
判定: APPROVE
ブロッカー
なし
提案(非ブロッキング)
codex-cli 0.163.0がmodel/listにこれらのフィールドを持つ最初のバージョンで、それより古いバージョンでは省略される」という記述は誤りです。上流のcodex-rs/app-server-protocol/src/protocol/v2/model.rsを確認したところ、rust-v0.142.5とrust-v0.155.1の時点でsupported_reasoning_efforts/default_reasoning_effort/additional_speed_tiers/service_tiers/default_service_tier/is_defaultがすでにあります。「0.163.0 で live 検証済み」とだけ書き、「first version」の主張は削るのが正確です。CodexModelsTest.kt:11 の「live 0.155.1」という fixture 注記(このフィールドを含まない)とも矛盾しています。effortとserviceTierを明示して送るので、ユーザーが~/.codex/config.tomlに書いたmodel_reasoning_effortやservice_tierが上書きされます。以前は送っていなかったので、これは挙動の変化です。設計として意図したもので docs にも理由が書かれていますが、PR 説明に一言あると親切です。代案もあります。0.155.1 以降のTurnStartParamsには、そのターンだけに効いて後続ターンに残らないserviceTierForTurnがあります("default" でスタンダード指定、省略時はスレッドの tier を継承)。tier をこちらで送れば、tier を毎ターン強制する必要がなくなります。effortには同等の仕組みがないので、現状の方式が妥当です。sendMessageのモデル解決(キャッシュ未ヒット時にmodel/listを 1 回だけ再取得する、isDefault経由で既定モデルを使う、再取得失敗なら次のターンで再試行する)を直接テストしていません。resolveCodexTurnSettings単体はテスト済みです。fake のCodexRuntimeを差し込むのが難しいなら、resolveKnownModelと再取得の判定を純関数に切り出してテストする余地があります。{ enabled -> chatViewModel.setFastMode(enabled) }は、周囲と同じくchatViewModel::setFastModeと書けます。(Boolean?) -> Unitは(Boolean) -> Unitに代入できます。ラムダは再コンポーズのたびに作り直されるので、周囲の書き方に揃えると良いです。fastModeDescriptionの KDoc に「toggle's subtitle」とありますが、実際の用途はスクリーンリーダー向けの contentDescription です(ChatHomeScreen のFastModeChip)。KDoc を実際の用途に合わせてください。initialCodexFastModeは作成時に一度だけ読まれます。Fast チップが出るのは Codex の VM だけなので、今のところ実害はありません。将来ほかの runtime でも fast mode を出す場合は、AppPreferences.codexFastModeの StateFlow を購読する形にした方が安全です。チェック済み項目
/tmp/opencode/review-upstream-codex-effort-fast.diffと.summary.txt(2 コミット、24 ファイル)を全 hunk 読み、worktree の実ファイルと照合しました。chat_thinking_level_none/minimal/ultra、chat_fast_mode、cd_fast_mode、cd_fast_mode_with_description)が values と全 7 ロケール(ar/es/fr/ja/pt-rBR/ru/zh-rCN)の計 8 ファイルにそろっていることを rg で確認しました。%1$sのプレースホルダもすべて一致しています。Kotlin 側にハードコードされたユーザー向け文字列はありません(Preview 内のサンプル文字列は除く)。ChatViewModelの 2 つの構築経路に同じ引数(onCodexFastModeChanged/initialCodexFastMode)が入っていることを確認しました(di/ViewModelModule.kt:22-29 と ui/AndCodeApp.kt:313-314)。AppPreferencesRepository/SecureSettingsRepositoryは AppModule.kt:52-54 で single として登録されています。SecureSettingsRepository.codexFastModeは remove と put で「未選択」と「選択済み」を区別しています。AppPreferencesRepositoryの初期読み込み(:98)とsetCodexFastModeの state 更新も確認しました。rust-v0.163.0とrust-v0.155.1で、TurnStartParams.effort/serviceTierが後続ターンにも残る(sticky)こと、SERVICE_TIER_DEFAULT_REQUEST_VALUE = "default"が明示的なスタンダード指定であること、ServiceTier::from_request_valueが"fast"と"priority"の両方を受け付けること(旧additionalSpeedTiers由来の"fast"も有効)、ReasoningEffortに none/minimal/…/ultra があることを確認しました。PromptRequest.serviceTierは OpenCode 側に漏れません。OpenCodeApiClient/OpenCodeV2ApiClientはフィールドを 1 つずつ組み立てています。PromptRequestの他の呼び出し元(QuickInput、Schedule、Voice、OpenCodeBackend.command)は既定の null のままで、Codex 側でモデル既定値に解決されることも確認しました。selectConfigurationの唯一の呼び出し元(AndCodeApp.kt:614)が fast 関連の 3 フィールドを渡していること、モデルを切り替えるとselectedVariantがリセットされる既存の挙動が保たれていることを確認しました。CodexTargetの新しいキャッシュは@Volatileの参照を差し替えるだけで、新しい coroutine scope は作っていません。.worktree/feat/...配下)について問題はありません。