mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* 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
41 lines
2.1 KiB
TypeScript
41 lines
2.1 KiB
TypeScript
import { expect, test } from 'vitest'
|
|
import { agentSessionFailureWords } from '../../../src/shared/agent-session-failure-words'
|
|
import { agentSessionFailureFact } from '../../../src/shared/agent-session-failure'
|
|
import { importReleaseCheckoutModule, materializeReleaseCheckout } from './release-checkout'
|
|
|
|
// A release that predates the `cliMissing` failure kind. Pinned, not the newest tag: what it must
|
|
// do is show the host's own sentence for a kind it cannot place, which no later release contradicts.
|
|
const BASELINE_REF = 'v1.4.218'
|
|
|
|
type OldStatusBlock = (body: Record<string, unknown>) => { text: string; failure?: unknown }
|
|
|
|
// Loads a real old build, including cold extraction and transforms.
|
|
test("an older client shows this host's sentence for a start whose CLI was not found", async () => {
|
|
const words = agentSessionFailureWords(agentSessionFailureFact('cliMissing'), {
|
|
agentName: 'Codex',
|
|
surface: 'row'
|
|
})
|
|
expect(words).toEqual({
|
|
text: "Codex wasn't found on the computer running this chat. Install it, or check its Command in Settings → Agents.",
|
|
failure: { kind: 'cliMissing' }
|
|
})
|
|
|
|
const checkout = await materializeReleaseCheckout(BASELINE_REF)
|
|
const [failure, statusBlock] = await Promise.all(
|
|
[
|
|
'src/shared/agent-session-failure.ts',
|
|
'src/shared/structured-agent-session-status-block.ts'
|
|
].map((path) => importReleaseCheckoutModule(checkout, path))
|
|
)
|
|
const isKind = failure.isAgentSessionFailureKind
|
|
const render = statusBlock.structuredAgentSessionStatusBlock
|
|
if (typeof isKind !== 'function' || typeof render !== 'function') {
|
|
throw new Error('the pinned release lacks the failure-kind reader or the status block')
|
|
}
|
|
expect(isKind('cliMissing')).toBe(false)
|
|
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pinned release's export, checked to be a function above; a changed shape fails the assertions below.
|
|
const block = (render as OldStatusBlock)({ kind: 'status', tone: 'error', ...words })
|
|
expect(block.text).toBe(words.text)
|
|
expect(block).not.toHaveProperty('failure')
|
|
}, 120_000)
|