* fix(native-chat): disable Send for unavailable agents
* test: update sign-in copy expectations
* fix: show automatic chat availability only on Send
* fix: keep signed-out evidence when a live session lists models
* refactor: split Send availability presentation and tests
* refactor(native-chat): keep the sign-in verdict on the catalog's failure record
The probe's "signed out" / "CLI missing" verdict now lives on the account's
existing model-catalog failure record instead of two extra maps, with the
failure TTL as its only lifetime:
- Only the probe sets or clears the verdict; a chat's own model listing
(which works while signed out) never clears or renews it. An aged-out
verdict makes the next read re-probe, however fresh the catalog.
- A healthy fresh catalog is no longer re-probed every 30 s; it keeps the
normal 10 min fresh window. A Claude start refused as signed out ages the
catalog so the next read re-probes.
- A waiting picker again answers from whichever catalog lands first and never
waits on a probe. A read that still owes the probe's verdict says so, and the
composer waits for it once with the new optional waitForAvailability param.
- The open-time start-failure line and its transcript row are hidden only
while Send is disabled for the same reason; otherwise they show as before.
- A host verdict lifetime longer than the client's hold is clamped, not
dropped. The catalog read watches only the account settings and the paired
host's contact instead of every store write.
* fix(native-chat): one send-gate rule, and a missing CLI as its own failure
- The sign-in/CLI verdict is gated once where the session hands it to the
composer: while a turn runs it neither shows nor refuses, so a follow-up
queues as before; goal mode is held by the same gate. The start-failure line
and row hide only while that same value is up.
- A start whose CLI was never found (a direct spawn's ENOENT, or the one a
provider supervisor reports) is now a `cliMissing` failure with the disabled
Send's sentence, instead of a generic "couldn't start" beside it. An older
client shows the host's sentence for the new kind.
- Signed-out words for a slash command keep "Run /x again".
- The Claude catalog reads its models only from the reply to its own
list_models request, never from the initialize reply.
- The Codex probe lists models while its account check runs; a signed-out
verdict still wins.
- The missing-executable check moves to provider-process, out of native chat.
* test(native-chat): signed-out command failures name the command to run again
* fix(native-chat): probe the Codex home a launch would read
- Before the catalog probe's read-only app-server reads Orca's shared Codex
home, it runs the same sync a launch and the usage poll run, so a
`codex login` made in ~/.codex is seen at once instead of leaving
"Run codex login" up until something else re-syncs (forever on a headless
host). Per-account homes are left alone.
- A "no account" answer counts as signed out only when the home's auth.json is
absent or holds no credential; one caught mid-write by a token refresh, or
holding a credential, is unknown.
* test(codex-accounts): type the node:os mock with a type import
* fix(native-chat): keep the sign-in verdict until a newer answer replaces it
- No gap while signed out: the host serves an aged verdict while the probe that
re-derives it runs, and the composer never clears its last answer before a
read; only an answer without a verdict, an error, lost contact or an account
change does. Send stays disabled with its reason through every re-check.
- After sign-in the stale "isn't signed in" start-failure line stays hidden:
a probe that found the account fine now says so (`accountVerified`), and the
line shows only when the host has no answer at all.
- A Claude start refused as signed out, or any start whose CLI was never
found, is recorded as that home's verdict, and the chat reads it once when
that start failure arrives.
- The re-probe mark is read only where the catalog read starts its probe, so a
Codex chat's own picker no longer re-lists for it on every read.
- Choosing an account or signing in again in Accounts settings re-derives the
verdict at the next read; the account key now compares settings by value.
- A re-read that brings an identical catalog keeps the option state, so focus
re-reads re-render nothing.
* fix(native-chat): Codex's signed-out words name only the step that unblocks this chat
A chat is pinned to the Codex home it started in, so choosing another account
in Codex Accounts settings never unblocks it. The system-account sentence now
says only to run `codex login`, in every locale.
* refactor(native-chat): give an account's sign-in status its own owner
The sign-in verdict lived inside the model catalog's failure records, so
the store needed guards for who wrote a failure, which listing may clear
it, and whether a verdict was served while re-probing. Each guard fixed one
way a chat's own listing or an older probe overwrote the account's answer.
The answer now lives in AgentAccountStatuses: one in-memory, capped status
per account home (ready, not signed in, or CLI missing). Only the
session-less probe, a refused start and an account change write it. A
blocked answer older than 30 s, or one an account change marked, makes the
next read start the probe whatever else is listing, and it is served until
that probe answers. Older evidence never replaces newer. The catalog's
failure code is main's again, and agent-model-catalog-failures.ts is gone.
The wire carries one optional `availability` field in place of
`unavailable` and `accountVerified`. The renderer holds its answer per
host, agent and chat, re-reads only on that agent's account inputs, and
re-reads on a timer only while blocked. After an account change, Send
stays disabled with the previous words until the re-probe answers, then
changes once. Account switches and re-sign-ins mark the status before the
settings change a chat's read follows.
* fix(native-chat): re-derive the sign-in answer after an account change mid-probe, and back off while blocked
Round-4 review findings on the account status owner:
- An account switch or re-sign-in made while a probe was running left the
old answer up for up to 30 s: the next read joined that probe, whose
answer was then dropped as older than the change. A read now chains
exactly one fresh probe after a probe that began before the change.
- Removing the selected Claude account swapped another sign-in into the
shared config dir without marking the answer; it now marks it first.
- Evidence was ordered by wall-clock time, so a clock that stepped back
froze a blocked answer. It is now ordered by a sequence and aged on a
monotonic clock; a negative age reads as due.
- A chat left open while signed out started a CLI probe every 30 s.
Probes that keep finding the same block now space out (30 s, 60 s,
2 min, 4 min, then every 5 min), carried to the client as recheckInMs.
An account change or a different refusal starts the hold over.
- The renderer dropped no settled wait from its leave set; it now does.
- A failed Claude probe logged its whole stdout, which now includes the
initialize reply naming the account. Control replies are dropped from
the log and the failure message.
- The account services take the status re-check as an injected
onSignInChanged hook instead of importing the native-chat store.
- Imports moved below file headers; comments no longer say "ready"
means re-checked after the failure.
* test(claude-accounts): say why the runtime-auth fakes are cast where the hook was added
* fix(native-chat): type the held-answer handle as the account status
* fix(native-chat): re-check a block when someone returns, and catch an account change during a home's first probe
Round-5 review findings:
- After a chat sat blocked for a few minutes, the backoff made every
read wait out a hold of up to 5 minutes, so someone who ran `/login`
or `codex login` and came back still saw Send disabled for minutes.
The backoff now applies only to the client's own timer read, which
it marks with an optional `scheduledRecheck` param. A read from
someone looking (window focus, the pane becoming visible, a new chat,
a start failure) re-probes once the block is older than 30 s. The
param is sent only on that timer, which only a blocked `availability`
answer arms, so a host that predates the param never receives it.
- An account change made during a home's first probe marked nothing,
because only existing statuses were marked, so the old account's
answer was written as current. Each agent's last account change is
now one sequence number that every status and running probe is
compared against; statuses are no longer mutated in place.
- Removing an account that nothing had selected no longer re-checks.
* refactor(native-chat): keep the sign-in and missing-CLI words in their own module
Main's retry wording pushed the failure-words table past its line budget.
* refactor(native-chat): build the structured chat's view session in the view-state module
Main's send-reveal work brought NativeChatStructuredSession.tsx and the
session controller to their line budgets.
* fix(native-chat): keep a stated start line hidden through a running turn, and re-probe an account changed mid first probe
- A chat whose open-time start failed signed out had that line hidden
while the host said the same. When a new signed-out answer landed
during a running turn, the line came back for the whole turn: the
hide read Send's gate, which a running turn suspends. The hide now
reads the host's answer itself (blocked for the same reason, or
ready); Send's gate still steps aside while a turn runs. Both are
views of the one answer the session holds.
- An account change during a home's first probe dropped that probe's
answer but left nothing to re-check the new account, and the picker's
reply said no probe was pending, so the chat stayed unknown until the
catalog aged out. A read now chains one fresh probe whenever the
running probe began before the change, and its reply says that probe
is still owed so the client asks once more for its verdict.
- The client's timer read no longer carries `scheduledRecheck`. The
timer already fires only when the host's backed-off `recheckInMs`
says, which is past the 30 s after which any read re-probes, and
every answer re-arms it. One fewer strict-schema param.
* build(cli): typecheck the missing-executable check the provider process now imports
Main's #25753 added managed-provider-process.ts to the CLI project, which
imports this PR's provider-executable-missing.ts.
* fix(native-chat): disable Send with the fix when Claude or Codex is signed out or missing
What you see: in a native Claude or Codex chat whose CLI is signed out, or not installed on the
computer running the chat, Send is disabled and its tooltip (hover or keyboard focus) says how to
fix it, in words that match the account ("Codex isn't signed in. Run `codex login`.", "This
Claude account isn't signed in. Sign in again in Claude Accounts settings.", "Claude isn't
installed on the computer running this chat."). A message refused after sending gets the same
words. On desktop the open-time "Chat could not be started" line, its Retry and its transcript row
no longer repeat these two reasons, unless a message of yours is still waiting on that start.
An unknown answer never disables Send, and while a turn runs Send stays Stop and Enter queues.
How: the host's existing session-less model-catalog probe also says why no chat can start.
Claude's probe child sends initialize before list_models and applies the rule chat start already
uses (token source none with no API key); Codex's probe asks account/read beside model/list; a
spawn that finds no executable is "CLI missing". The probe's failure record in the catalog store
carries that reason, and every catalog reply carries it as an optional `unavailable` field. Only
a probe writes or clears it: a live chat's own listing (which a signed-out Codex still answers)
never does. It lasts the store's 30 s failure lifetime; after that the next read starts or joins
the probe, serving the held reason meanwhile, and only a read that already asks to wait gets the
probe's answer. A change to the managed-account settings expires it. Before probing the shared
Codex mirror home, the probe runs the same sync a launch does. The renderer keeps the latest
answer until the next one replaces it, and only while blocked re-reads on window focus and when
the chat gains focus. No status store, timers or new request params.
* fix(native-chat): no picker loading while Send re-checks a sign-in, and plain keyboard focus on the disabled Send
A chat whose account is signed out and has no model list no longer shows the model picker as
loading each time it re-checks the sign-in after 30 s: the re-check is for Send, so a reply that
already carries the verdict ends the picker's wait. The disabled Send's wrapper takes keyboard
focus (so its tooltip opens) without becoming a second button; the inner button keeps its label.
Trims tests that repeated rules other tests already prove.
* fix(native-chat): lift the Send gate whenever Stop shows, including while a queued card is about to send
The composer shows Stop while the host's queue is about to send its next card, but the sign-in
gate stayed on in that window, so Enter was refused with no message.
* fix(native-chat): never disable Send for a sign-in or missing-CLI verdict; show a dismissible notice instead
The host's pre-send check can be wrong while the chat would work (saved
Arguments it never sees, project settings it can't read, gateways that
accept a signed-out Codex), and a disabled Send left no way past it.
- Delete the Send gate: the composer, Enter and goal mode no longer read
the verdict, and the Send tooltip reason and focusable wrapper go back
to main.
- Show Codex not-signed-in and Claude/Codex not-installed as one entry in
the chat's existing notice card. Dismissed per verdict; shows again
when the verdict changes or clears and returns; steps aside when the
failed start (launch line or latest failed-start row) says the same.
- Claude: drop the probe's signed-out verdict (initialize-on-probe stdin,
request-id filtering, control-reply log scrubbing). The chat start's
refusal, with its account kind, is the only Claude sign-in signal.
Missing-CLI from the probe stays.
- Restore the failed-start line and transcript row for these causes
(no more hiding on desktop) and drop the chat-root focus re-check; a
window-focus re-read stays while a notice is showing.
* fix(native-chat): re-check the notice's reason after a chat starts, keep a signed-out Codex's model list
Review round 1 of the notice rework.
- A chat that proves its start (at acquire, or on its later "started"
event) marks the catalog's held reason for that account due, so the
next read re-probes instead of trusting it until the TTL; the chat
re-reads when a turn starts or ends while a notice shows.
- The Codex probe keeps a model list it got beside a signed-out reason,
so a gateway user with a wrong verdict keeps the picker list.
- Failure records move to their own module; an expired failure with no
reason is dropped, and expiry drops reason-less failures.
- Notice: any verdict change resets the dismissal; signed-out is
Codex-only; the say-once rule ignores a failed start a turn has run
past.
- Missing-CLI copy: "{{agent}} wasn't found on the computer running this
chat. Install it, or check its Command in Settings → Agents." (6
locales).
* fix(native-chat): only an acquiring start re-checks the catalog reason; drop the unreachable Claude filter
Review round 2 of the notice rework.
- A re-attach to a live child proved nothing new, so only an attach that
acquired a child which proved its start marks the reason due.
- Reword: a start proves the program exists; for a Codex sign-in it only
prompts an earlier re-check.
- Drop the notice's Claude signed-out filter: no host sends that verdict.
- Tests: a re-attach does not notify; a turn ending re-reads once more.
* fix(native-chat): never newly show a catalog reason the host is still re-checking
After signing in to Codex, a new chat flashed the old "isn't signed in"
notice for about half a second: past its 30 s lifetime the host answers a
plain read with the held reason while its probe re-checks. A chat now
adopts such a reason only when it already shows it; otherwise the joined
read's answer decides.
* test(native-chat): follow main's harness and the clearer Claude sign-in words after the merge
* test(native-chat): run the start-proof catalog test in the Node runtime project with the other SQLite host tests