Repository navigation
Every agent reports its learned default model once, when a chat starts - #26982
Conversation
…tart Every options read re-reported a no-pick chat's configured default: ACP re-derived it from the session's current state, and Pi re-sent its start snapshot. A chat left open kept writing its start model back over a newer default another chat had learned since (for example after the user changed the agent's own default), so new chats named the old model and then jumped. A ready child's acquisition now carries its start listing, with the default its start resolved after restoring the chat's saved picks, and the host saves it once when the owner is proven (a starting child already hands it on its `started` event). Options reads carry the model list only. Grok, OpenCode, OMP and Pi share this path and the one no-pick rule.
The configured-effort fallback took the older listing's menu whenever it had the effort, so an older background listing could bring back an effort the agent's own newer menu no longer offers. It now falls back only to the chat's own listing.
…s folder picks nothing Pi reads an empty settings file as no settings, and a .pi/extensions folder holding only .DS_Store or .gitkeep loads no extension, so neither keeps the "Model" placeholder any more. The BOM strip is written as an escape. Tests cover the packages key and an extensions folder that can't be read.
… start's settings readback Claude's configured default rode on every options read, so an open Claude chat with nothing picked kept writing back the model its settings applied when it started, over a newer default another chat had learned since. Its default is known only once the settings readback after `started` lands, so that readback's `options-reported` event now carries the start listing with the resolved default, and the host saves it once for the child that reported it. Options reads carry the model list only, as for every other agent.
…h, and never fails the start Claude's settings readback, its one report of what the config resolved, was checked against the host's child index as it arrived. A readback that beat a slow attach found no child yet and was dropped for good. It now waits in the session's serialized step behind that attach, as `started` does. Handing a ready child's start listing to the catalog ran inside the acquire's cleanup scope, so a throw there would have torn down a proven child. It is bookkeeping: a failure is logged and the start proceeds.
…ption; Pi's start listing is read-only
No behavior change: claudeCatalogListing takes { withConfiguredDefault } instead
of a trailing boolean, and PiRpcSession exposes its start listing through a
getter over a private field.
Review summaryReview: one full review round, with no P0/P1 findings. The findings were fixed in c7f080f and ef7aed8:
Visual proof (in the body): live before/after on Windows with stand-in Pi and Grok agents. Each screenshot was checked against the stand-in logs. On main, after switching back to an older chat, the next new chat shows the old default. With this PR it shows the newest one. The proof also shows that a new Pi chat's first frame names its learned default; #26821 merged without that proof. Real agent homes were hashed before and after, and were unchanged. Found during QA, not caused by this PR: a new Grok/Pi/Codex chat's effort picker shows a blank "Effort" until the agent starts. Main has the same gap: when no agent is running yet, the chat's options answer drops the default effort for every agent except Claude. A separate follow-up PR fixes it. CI: cross-version wire compatibility fails on main's own "kiro" search tests; branches that don't touch search show the same 7 failures. verify only reports that result. package (linux) failed pulling from Docker Hub (504) and was rerun. Everything else passes. The PR's files, merged with current main, pass lint, including file length. Not verified: real agent CLIs this round (the real Claude CLI test passed 13/13 on 98d8542), SSH/WSL hosts, and mobile. |
… start #26982 reported a no-pick chat's configured default once, from a ready child's acquisition. With every acquire now returning at spawn, the start listing rides on `started` instead: ACP's after its option restore, Pi's from the model list its start reads. The host saves it inside the `started` step, and a failure to save it is logged, never the start's. Claude keeps its default on the settings readback, saved behind the attach. The acquisition hook and its field are gone. Also moves the start-listing saves and the Stop note's turn scope into their own modules to keep the merged files under the line limit.
#26982) * fix(native-chat): a chat says what its config resolved once, at its start Every options read re-reported a no-pick chat's configured default: ACP re-derived it from the session's current state, and Pi re-sent its start snapshot. A chat left open kept writing its start model back over a newer default another chat had learned since (for example after the user changed the agent's own default), so new chats named the old model and then jumped. A ready child's acquisition now carries its start listing, with the default its start resolved after restoring the chat's saved picks, and the host saves it once when the owner is proven (a starting child already hands it on its `started` event). Options reads carry the model list only. Grok, OpenCode, OMP and Pi share this path and the one no-pick rule. * fix(native-chat): only a chat's own effort menu outranks a newer one The configured-effort fallback took the older listing's menu whenever it had the effort, so an older background listing could bring back an effort the agent's own newer menu no longer offers. It now falls back only to the chat's own listing. * fix(native-chat): an empty Pi settings file or dotfile-only extensions folder picks nothing Pi reads an empty settings file as no settings, and a .pi/extensions folder holding only .DS_Store or .gitkeep loads no extension, so neither keeps the "Model" placeholder any more. The BOM strip is written as an escape. Tests cover the packages key and an extensions folder that can't be read. * fix(native-chat): Claude says what its config resolved once, with its start's settings readback Claude's configured default rode on every options read, so an open Claude chat with nothing picked kept writing back the model its settings applied when it started, over a newer default another chat had learned since. Its default is known only once the settings readback after `started` lands, so that readback's `options-reported` event now carries the start listing with the resolved default, and the host saves it once for the child that reported it. Options reads carry the model list only, as for every other agent. * fix(native-chat): a start's learned default is saved behind the attach, and never fails the start Claude's settings readback, its one report of what the config resolved, was checked against the host's child index as it arrived. A readback that beat a slow attach found no child yet and was dropped for good. It now waits in the session's serialized step behind that attach, as `started` does. Handing a ready child's start listing to the catalog ran inside the acquire's cleanup scope, so a throw there would have torn down a proven child. It is bookkeeping: a failure is logged and the start proceeds. * refactor(native-chat): name the Claude listing's configured-default option; Pi's start listing is read-only No behavior change: claudeCatalogListing takes { withConfiguredDefault } instead of a trailing boolean, and PiRpcSession exposes its start listing through a getter over a private field. (cherry picked from commit 7450f40)
ELI5
Orca learns which model each agent runs by default from a chat you start without picking one, and then names that model on the next new chat's first frame (#26407, extended to Pi in #26821). Two reviews of #26821 found that a chat left open keeps re-teaching the model it started on. So after your agent's default changes, every new chat names the old model and then jumps to the right one, for as long as that old chat stays open. They also found that Grok, OpenCode and OMP used different timing from Pi for the same rule (Claude had the same per-read timing as Grok), plus a few small gaps in Pi's project-config check. This PR makes every agent that learns a default (Claude, Grok, OpenCode, OMP and Pi) report it exactly once, when the chat starts, and fixes the small gaps. Codex learns nothing from chats: its background listing reads its config directly.
What Changed
The problem
/model, or Claude's settings, in a terminal). Chat B starts on Z and teaches Z. Then A's next turn writes Y back. Every new chat now opens on "Y" and jumps to "Z", until A is closed.What you see now
.pi/settings.json, or a.pi/extensionsfolder holding only dotfiles (.DS_Store,.gitkeep), no longer keeps the "Model" placeholder. Pi reads both as nothing.xhighsurvives Pi's coarser background listing) can no longer revive an effort that the agent's own, newer menu has dropped.Mechanism
catalogListing: the models it started with, plus the shared no-pick rule's answer (unpickedSessionConfiguredChoice). That answer is taken once, after the chat's saved picks are restored.onStartCatalogListingin the attach flow.startedevent, and only learns what its settings resolved from a settings readback right after. That readback'soptions-reportedevent now carries the start listing with the resolved default. The host saves it once, for the child that reported it.started), so a readback that arrives before the attach finishes indexing the child is still saved. Saving a start listing is bookkeeping: if it throws, the failure is logged and the start goes ahead.unpickedSessionConfiguredChoice). Claude shares the timing (one report per start, never on reads) but keeps its own equivalent test of what was picked (configuredClaudeDefault: no launch model, no model pick, and the model its settings readback says it applied).session/newwith its full model list and current model, built synchronously, and sendsconfig_option_updateonly after a model or thinking change. Checked by reading the installed packages, without running them: OMP 18.4'snewSessionbuilds the list from the session's models, and pushes updates only onmodel_changed/thinking_level_changed. OpenCode 2.0.14'snewSessionreturns the options after creating the session with the configured model, and never sends the update. OpenCode 1.18.31 sends it only after a model pick. Grok's captured answer carries its options. An OMP that lists no models (the captured Windows no-model case) reports no model, so nothing is learned, as before.\uFEFFinstead of a raw invisible character.structured-agent-session-adapter.tsintostructured-agent-session-acquisition-errors.ts(re-exported, no import changes). That keeps the adapter types file under its line limit with the new field.Why
The owner asked for one rule for every agent. The common pattern derives what a new chat shows from facts the host already holds, and a fact about how an agent's config resolved belongs to the moment it resolved: the start. Reporting once at the start removes the stale write-back for every agent at the root. The alternative, having each read hand its snapshot over once and then clear it, would keep the default tied to whenever the pane happens to read. It would also miss chats whose options are never read.
Differences from the common pattern
configuredClaudeDefault) instead of the shared function; the two agree on every case: a launch or saved model pick means nothing is learned, and an effort pick means no effort is learned.config/read), so a chat has nothing to teach.Linked Issue
Follows up #26821 (its two review rounds) and #26407. No separate issue.
Visual Proof
Captured on Windows in a hidden dev build of the PR head (
ef7aed88dea) and of its base on main (b8a0554), with stand-in Pi and Grok agents on a fresh profile. Steps, for each agent: default X, open chat A and send (A starts on X); change the default to Y, open chat B and send (B starts on Y); switch back to chat A; open new chat C and capture its first frame before sending anything. Pi: X = GPT-6.1 Sol, Y = Claude Sonnet 4. Grok: X = Grok Build, Y = Grok 4.7.Pi, before (main): chat C opens on GPT-6.1 Sol, the old default, although chat B had started on Claude Sonnet 4.

Pi, after (this PR): chat C opens on Claude Sonnet 4, the newest default.

Grok, before (main): chat C opens on Grok Build, the old default.

Grok, after (this PR): chat C opens on Grok 4.7, the newest default.

Pi, this PR: once one Pi chat has started, the next new Pi chat names that default (GPT-6.1 Sol) on its first frame, before its own agent starts.

Testing
Run by file name on macOS, on the Node runtime where the repo requires it:
src/main/pi/rpc-configured-default.test.ts): chat A started on GPT-6 reads its options again after chat B taught Claude Sonnet 4, and the next new chat still names Sonnet. Also:acp-structured-host-configured-default.test.ts, real Grok probe, adapter and host): chat A runs 4.6; a newer chat teaches 4.7; A reads its options again; the next new chat names 4.7. Save a new chat's first message during create #26332's "created with its first message" case now also checks that the default is learned before any options read.acp-structured-host-live-listing.test.tsexpects the start's save plus the read's, with no default on the read.claude-structured-configured-default.test.ts, real Claude adapter over a scripted CLI): chat A teaches Sonnet; a newer chat teaches Opus; A reads its options again, and its listing carries no default, so Opus stands. The other Claude configured-default tests now learn from the start's report.structured-agent-session-provider-started.test.ts(real Claude adapter and host) checksstartedsaves the listing without a default, the settings readback saves it with one, and a later read saves none, including for Save a new chat's first message during create #26332's chat created with its first message.claude-structured-effort-default-at-rest.test.tslearns from the start's report.agent-model-catalog-entry.test.ts, new): a chat's menu wins over a newer background menu that lacks the learned effort; an older background menu never wins over a chat's newer one.agent-project-model-override.test.ts):packagescounts;Fail without the change (checked by putting the old code back and running the tests):
Also passing: every ACP and Pi test file, the host attach, acquisition, provider-started and create tests (including #26332's), the catalog tests, the first-frame tests, the Claude and Codex configured-default tests, the renderer learned-default test and the SQLite runtime boundary test (291 files, 2942 tests, including every Claude test file except the two real-CLI ones).
98d85423eee:claude-structured-real-cli.test.tsandclaude-structured-real-cli-fold.test.ts, 13 of 13 pass. Since then the Claude files changed only by renaming a parameter (a boolean became{ withConfiguredDefault }), with no behavior change, so it wasn't run again.Checks:
pnpm tc:node,pnpm tc:weband oxlint on changed files pass.orca-ci-checks: 22 of 23 pass. The one failure isaudit:code-quality:native, the existing import-cycle warnings inmobile/src/transportandmobile/src/terminal/document; no file of this PR is involved.CI: see the status below;
cross-version wire compatibilityis red onmainitself. It fails 7 tests inqoder-history-search-downgradeandsession-search-agent-set-downgrade, because an old build's session-search agent list lackskiro. Run 37988257228 (brennanb2025/native-chat-agent-checks-use-registry, based on main2cb2bef67e3, 4 files changed, none touching search) fails the same 7. The aggregateverifythat requires that job fails with it. Since98d85423eee,package(Linux) fails while pulling thenode:22-bookworm-slimimage from Docker Hub (429 Too Many Requests, then onef7aed88deaa504 Gateway Timeoutfrom Docker's auth server). It passed on2427a7c5bd7, andpackage (windows)passes.Not verified: the running app, real agent CLIs, SSH/WSL hosts beyond the shared code paths (the save and the workspace check run on the execution host), and the phone.
AI Disclosure
Review
Agent skill upstream boundary
Notes
Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)