mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 00:02:24 +00:00
rm-tooltip
18
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
889c2b562f |
feat(mobile): say why a host is unreachable instead of "Connecting via Relay…" (#21566)
* feat(mobile): say why a host is unreachable instead of "Connecting via Relay…"
The home-screen host row and host header showed "Connecting via Relay…" for
as long as the desktop stayed unreachable, even when every relay dial had
ended with the cell's 4404 host-offline close. A user's diagnostics export
showed 25 such dials over 25 hours behind that label, and the diagnostics
report itself said "No single failure cause" because relay dial failures
were not recognised and every app resume emptied the evidence window.
Relay close codes now map to a closed RelayHostReachability verdict
(signed-out, host-offline, credential-refused, unreachable), latched after
two consecutive identical dial failures and cleared only by an authenticated
session. The existing signed-out close reason becomes a member of the same
verdict instead of a parallel boolean. classifyConnection renders each
verdict as a label plus a detail line ("Host 1 is offline" / "Check it's
awake, Orca is running, and you're signed in").
Relay dial failures carry their close code as a structured field on the
connection log entry, so the diagnostics analysis names the cause without
parsing error text, and an app resume no longer hides the last failure: it
is reported with a "Before the app last resumed" qualifier and is never a
sendable incident.
* test(mobile): re-record RPC goldens at the new baseline
Only header lines change: the baseline pin on every golden and the
adapterSha256 on the twelve goldens whose mount adapters gained the
getRelayHostReachability context method. No checkpoint moved, which also
shows the commits between the old and new baseline changed no observed
RPC behaviour.
* fix(mobile): tell a refused relay credential to re-pair, not to find the same network
A direct session also rotates the credential, but telling the user to
connect on the same network once explains the mechanism instead of giving
an action, and re-pairing is the one remedy that works from anywhere.
* fix(mobile): let the newest relay failure win the diagnosis, and name the real stale boundary
Relay-path evidence still outranks a newer direct timeout, but among relay
failures the newest now wins: an older 4404 verdict no longer hides a newer
session close (which was also the sendable incident) or a director refusal.
The stale prefix names a network change when that, not a resume, was the
boundary.
|
||
|
|
47d107cf2e |
feat(mobile): hybrid shell route, dark behind a dev-only flag (OTA phase B, 4/4) (#21435)
* refactor(mobile): say whether a host status was readable, and carry its protocol window `useHostStatusGates` settled the same closed gates for a host that answered `status.get` with no capabilities and for one whose status nobody could read: both paths produced an empty capability list and an `ok` verdict. A caller that walls on a missing capability cannot tell those apart, and the mobile web bundle's wall is terminal, so it must never fire for the second. `statusReadable` distinguishes them. `hostProtocolWindow` exposes the two protocol numbers the hook already read for `evaluateCompat`, as the reply's own fields, so the bundle wall can evaluate its own window without a second `status.get`. Both are additive; no existing consumer changes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): add the hybrid shell flag and the generation path both loaders demand `orca:mobileWebShellEnabled`, default off and unreadable-is-off, in the same shape as the terminal autocomplete flag. `generationDirectoryPath` converts the store's `file://` uri to the absolute path the native shell view requires: both `MobileWebShellGeneration.load` implementations refuse anything without a leading slash, and `expo-file-system` only ever hands out uris. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the hybrid shell session as a pure reducer Every decision the route makes, as `(session, event) -> (state, effects)`: the capability wall, the lazy sweep and cache read, the manifest check, the cached build-id hit that skips paging, the offline open with no compat check, and the three recovery rules the native shell view's contract states. Pure, so the rules are table tests rather than a simulator run. Two latches sit beside the state because both outlive it: `retriedOnce` spans the delete and refetch that returns to `checking`, and `remountedOnce` spans a `ready` replaced by a `ready` under a new session id. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the hybrid shell route, dark behind a dev-only flag (OTA phase B, 4/4) Wires the four Phase B and A pieces together and adds no decision of its own. `h/[hostId]/web` sits inside the existing `HostProtocolGate` tree, so the native `desktop-too-old` wall still applies above it. With the flag off — every store build, since the only writer is a `__DEV__` Troubleshoot toggle — the route redirects to `h/[hostId]` and the screen is never constructed, so nothing is fetched, written or swept. The runner owns only the impure edges and checks an epoch before every dispatch, so an unmount, a host change or a retry abandons work in flight and aborts a download that would otherwise hold four of the host's read slots. The native view is keyed on the session id, which is what makes the reducer's remount a rebuilt WebView with every fence reinstalled. A census test pins who touches the flag: the route reads it, the developer row reads and writes it, and the key itself lives in one module. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the hoisted test doubles instead of asserting them The changed-code casting gate refuses `as` in new code, and these three were only widening an empty literal. An annotated `vi.hoisted` factory does the same job under a check. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): read a scheduled reconnect as an unreachable host, not a dial in progress Found on a simulator with the paired desktop stopped: the client never settles on `disconnected`. It dials, fails, schedules a retry, and cycles `connecting` -> `reconnecting` -> `connecting` with the delay growing to a minute. Mapping `reconnecting` to "still connecting" left a phone holding a verified cached generation on `checking` forever instead of opening it, which is the one case the offline rule exists for. `connecting` and `handshaking` are the first dial and still wait; everything else is unreachable. The mapping moves next to the reducer it feeds, because it is a decision and the runner is supposed to hold none. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): build the reachability stub instead of asserting it Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): move the shell session vocabulary into its own module Pure move, no behaviour: the states, events, effects and gates the reducer and its runner share now sit beside the reducer rather than inside it, so the transition rules have room to grow under the file's line budget. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): drop a shell effect result whose flow has been superseded Every restart of the flow bumps a number the effects of that run are stamped with, and a result echoes it back: a manifest read still in flight when the socket drops used to reject after the offline path had already opened the cached generation, replacing a displayed workspace with a download failure, and a status refetch arriving mid-check used to run the cache read and the download twice. The gates restart no longer clears the remount latch either; only the retry button does, so a reconnect cannot grant a second remount. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): census the flag across modules, not just src and app The native view tree was outside the scan, so a reader added there would have passed an assertion that reads as exhaustive. Proven by adding one to the shell view module: the census fails. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): let only the state that mounted the view hear the view A native batch reports two failures in a row, and the reducer applied both: document-load-failed started the delete-and-refetch, render-process-gone then made it terminal without a new flow, and the cache read the recovery had already asked for dragged the session back to checking behind a failure screen. A report arriving outside `ready` is from a view that is no longer on screen, so it changes nothing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): say a host status could not be read instead of spinning on it A transient status.get failure settles the gate unreadable and nothing probes it again, so the route sat on "Checking host" for as long as anyone left it there and Try again re-read the same settled answer. It now says what happened and offers no retry, and a status that does become readable picks the flow back up on its own. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): restart the flow on the verdict that changed, not on every gates object A reconnect cycle rebuilds the gates several times a second with the same answer in them, and each one re-swept the staging tree and flipped an offline screen to a spinner and back. Only a changed verdict restarts now, which is also why the gates effect has to depend on the host id: two hosts whose gates read identically would otherwise leave the second session in `checking`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): cover what only the shell runner can get wrong Three cancellations had no test: the epoch that stops a result reaching a session that is gone, the unmount cleanup that aborts the download, and the retry that does both before starting over. Each is now red under its own mutant. The download also re-checks the abort before it writes, since an abort landing between the fetch's last read and the commit would otherwise still put a generation on disk for a screen nobody is on. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the shell runner's refs after the commit, not during render React can replay or discard a render, so a handle written during one can run effects for a session that never existed. The client and the host cache key stop being refs at all; the effect handle is committed in an effect above every effect that dispatches. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep the hybrid shell flag unreadable outside a development build Development and release share a bundle id, and the iOS data container survives an install-over, so a flag a developer toggled on would follow the store build in and mount the shell on a deep link. The release read never reaches storage. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): drive the route's flag read as each build kind reads it The route test exercises the real preference read, so it has to say which build it is. A store build whose container kept a development toggle redirects. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): let a cache read that lands mid-dial wait for the compat check A connection still being made is not a host that cannot be reached. Opening the cached generation there skips the compat check the landing connection is what makes answerable, so only `unreachable` takes the offline path now. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): move the developer toggle only after its write lands The route reads the flag back from storage, so a switch that moved on the tap let the open button race the value that was being persisted. The switch and the button both stay put until the write settles, and a failed write keeps the previous position. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): say which build kind a test runs as without asserting on globalThis Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): undo a staged generation the abort reached before the commit The commit is the write staging cannot take back: it renames into the active slot and moves the host index. An abort landing while the bytes were being staged now removes the staged tree instead of activating it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): open the cached workspace when the link, not the bundle, cut a read short An RPC rejection can reach the reducer before the reachability change does, so the offline gate never fires and a phone holding a valid generation reads that the workspace could not be downloaded. A read that failed on the link now opens what is on disk, the same path offline takes; a verdict about the bundle, from the host or from the bytes, still fails. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): send a hybrid shell recovery through the same gate a start takes A view failure deleted the host cache and went straight back to the manifest check on whatever gates the ready session happened to be holding. Gates that arrive while a generation is on screen are stored without restarting, so after a reconnect whose status probe failed a ready session carried statusReadable false and an empty capability list, and the recovery's manifest check walled the host as bundle-unavailable: terminal, no retry, about a host that never answered. The gate is now one verdict both entries read, and recovery passes its delete through it, so an unreadable status lands on the status-unreadable message that re-arms when a readable gate arrives, and only a readable refusal still walls. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
1e7a69710d |
feat(mobile): update wall for the desktop-served mobile web bundle (OTA phase B, 1/4) (#21411)
* feat(mobile): decide whether a web bundle may open against its host A pure verdict for the bundle update wall, ordered so the answer names the soonest cause: a host with no bundle has no manifest to disagree about, and an unknown manifest schema makes the protocol window inside it unreadable. Same `?? 0` defaults as `evaluateCompat`, so an absent status field reads as the oldest host that could have answered rather than as permission. Every blocked verdict is terminal. There is no native workspace fallback, so each one carries the numbers it compared for the support breadcrumb. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): read an unknown bundle schemaVersion through to the wall The client reader pinned `schemaVersion` to the one schema this shell knows, so a future schema 2 failed the parse before `evaluateMobileWebBundleCompat` could call the shell too old. The user would have seen a transport error where the update wall belongs. The host's own manifest stays closed in both directions, where it is written. Goldens are unaffected: every recorded reply carries schema 1. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): give the block screen copy for the bundle walls One component still renders every wall. `updateSide` picks the app to update from the reason, so the copy and the store link cannot disagree, and a new reason is a compile error there rather than a mobile title over a desktop button. The existing protocol copy is unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): type the bundle protocol window the update wall compares The loose reader left `runtimeProtocolVersion` and `minCompatibleRuntimeProtocolVersion` as unknown index members, so the parsed manifest could not reach `evaluateMobileWebBundleCompat` without a cast. Both are now read as non-negative ints, and the reply-schema test pins it at the call site: the wall is invoked on a parsed manifest, so dropping either field stops compiling. A host that omits the window is now refused. Only a host too old to advertise `mobileWeb.bundle.v1` can send one, and the phone never asks such a host for a manifest. The probe test's fake manifest gained the fields it was missing, which is the typed reader catching its first stale fixture. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): say whether a bundle verdict actually checked a manifest `ok` meant two different things: the manifest was read and its window contains the host, or no manifest had been read at all. A caller that mounted on the second would mount an unchecked bundle, so `manifestChecked` separates permission to fetch from permission to open. The host-status input is now a `Pick` of `HostStatusReply` instead of a hand-copied pair. Both fields default through `?? 0`, so an upstream rename would have silently blocked every host rather than failing a build. Drops two assertions that restated the module's own literal back at it. What proves today's bundle opens is that the shared contract's schema version is a member of the supported list, so that is the assertion left standing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): offer a refetch, not a store, for a bundle the host outgrew `bundle-incompatible` on the mobile side means the workspace cached for this host is older than the host's client floor. A store update cannot clear that and a reconnect can, so the screen no longer sends the user to a download that would change nothing. The button is gone rather than relabelled, because the recovery is leaving this screen, and the note drops its "already updated?" opener for the same reason. `blockRemedy` replaces `updateSide` and is now passed to the copy instead of recomputed there, so the title, the body, and the button are decided once. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): drop the platform assertion from the block-screen mock The mocked `Platform.OS` was widened with an assertion so a test could switch stores. An annotation on the binding does the same widening in a position the compiler checks, which is what the changed-code quality gate asks for. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): say which bundle-compat default is fail-open, and drop a dead field Both comments claimed the two host-status defaults point the same way. Only `protocolVersion` is absent-means-oldest. An absent `minCompatibleMobileVersion` is `?? 0`, which is no floor at all, so the mobile arm is fail-open by design and matches `evaluateCompat`. A reader taking the old sentence at face value would have gone looking for a bug. `supportedSchemaVersions` had no consumer on the verdict: the block screen renders a title and body, and B4 reads neither. The exported constant stays, since that is what the wall is decided against. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
60a774c30c |
feat(mobile): client operations and dev probe for the desktop-served mobile web bundle (OTA phase A, 5/5) (#21374)
* chore(rpc-contract): provisional catalog entries for the mobile web bundle methods PROVISIONAL, and the only commit on this branch that must not survive the merge as written. `rpc-params-catalog.generated.ts` is generated from the host method registry, and A5's client operations cannot name `mobileWeb.bundle.manifest` or `mobileWeb.bundle.chunk` until A3 registers them: `defineRpcOperation` constrains `method` to `RpcMethodName`, which is `keyof typeof RPC_PARAMS_BY_METHOD`. These two entries are what the generator emits once A3 lands. After merging A3, run `pnpm run generate:rpc-params-catalog` and keep its output, not this. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): checked client operations for the desktop-served mobile web bundle Two `defineRpcOperation` descriptors over the A1 contract, both `require-result-or-throw` at `on-settle`: there is no partial success in a bundle read, and a salvage policy would produce a half-bundle that fails a hash check far from the cause. Readers are hoisted `looseObject`s that require only what this client reads, so a later optional member stays a Rule 1 addition for released phones; the host's own schemas stay strict. `dataBase64` is bounded by the contract's chunk size, so a host that overshoots is refused at the boundary rather than at reassembly. `readMobileWebBundleErrorCode` maps the host's six codes out of the thrown `code: message` diagnostic and answers null for everything else. Membership comes from the contract's own enum, which is built from its `hostUnionArms` record, so the arms here cannot drift from the host's union. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): fetch and verify a whole mobile web bundle over the paired connection `fetchMobileWebBundle` reads the manifest, pages every asset at the chunk size the host advertised, and verifies each reassembled asset against the manifest's sha256 before returning it. Nothing is cached and nothing is rendered: this is Phase A's proof that the pipe carries a bundle intact. Four asset reads run at once and no more, because the host refuses the fifth concurrent read on one connection with `mobile_web_bundle_read_limited`; paging inside an asset stays sequential, since the next offset is only known to be wanted once a reply says it is not the last. Every chunk reply restates its build, path and offset and the whole asset's length and hash, and all five are checked. A desktop that auto-updates mid-download answers a later chunk from a different build, and nothing else in the reply says so. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): dev-only troubleshooting row that fetches the mobile web bundle The Phase A proof that the pipe works on a device. Tapping it fetches the whole bundle from the paired desktop and reports the build, asset count, byte count and elapsed time, or the host's error code. `TroubleshootView` gains a `developerRow` slot and the route fills it only when `__DEV__` is true, so a shipped build mounts nothing: no host lookup, no client acquisition, no request. The row reuses the screen's existing button and check-row styles, so it adds no visual vocabulary. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): recording scenarios for the mobile web bundle operations Two families over the real product modules: `mobileWeb.bundle-manifest` drives the manifest descriptor alone, so the loose reader's verdict on one reply is the whole observation, and `mobileWeb.bundle-fetch` drives the paging flow over a two-asset bundle whose entrypoint spans two chunks. The fetch family's state carries the decoded bytes of every asset rather than a count. A reassembly that misplaces a chunk still has the right length, so only the bytes say so. Goldens land with the repin in the next commit: the recorder fences on the pinned tree, and these modules are not in it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the recording corpus and record the mobile web bundle goldens `--record` refuses on any tree but the pinned one, and the pin predates this branch's product modules, so the corpus is repinned to `bbf8264425` — the last commit here to touch a fenced path — and re-recorded whole, the way `rpc-recording/README.md` prescribes for a product change. The delta is the clean one that repin predicts. All 778 existing goldens move exactly one line, `baseline`, and nothing else: no body moved, no other header key moved, none was deleted. Nine are added, two pilot per family plus the five reply matrices the two families derive. The fetch adapter projects its result rather than returning it whole. The result carries a Map of Uint8Arrays, the observation refuses a non-plain object, and the first recording lost the settlement and filed an unhandled rejection in its place. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the fake host's params through a boxed field read The changed-code casting gate refuses the assertion the fake transport used to type its recorded params. Boxing the value the way `settings-read-operations.ts` does reads the same fields with no assertion, and a non-object params reads as absent instead of throwing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the corpus to this branch's last fenced commit The casting fix landed under `mobile/src`, which is a fenced path, so the pin no longer named the tree `--record` runs on. Repinned to `79c3eed6db` and re-recorded. Every golden moves the `baseline` header and nothing else, which is what a repin with no product change is: the edited file is a test, and no recording loads one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): mutation evidence that the fetch projection observes the bytes Writes every chunk at offset 0, so a multi-chunk asset reassembles as its last chunk over a zero-filled buffer. The length still matches the manifest, so only the sha256 check and the decoded bytes in the projection can see it, which is what the fetch family's state exists to show. The mutant is killed. `mutants/` is outside every golden digest, so this moves no recording. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): stop every worker's chunk reads the moment one asset fails `stopped` was read only between assets, so the other three workers paged their asset to the end after the fetch had already rejected: 121 chunk requests where 4 had been issued at the rejection. Each one holds one of the host's four read slots, so an immediate retry was refused with `mobile_web_bundle_read_limited` that only the abandoned workers caused. An internal AbortController now stands beside the caller's signal and is checked before every chunk request, not just between assets. Also pins the entry abort check, the overrun check with real bytes, the measured byte total, and a schema refusal whose message is prose rather than one of the six codes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the code anchor and both operation descriptors `RPC mobile_web_bundle_unavailable failed` separates the anchored reader from an unanchored one; the prose test that claimed to cover it had its first token at index 0, so the anchor was load-bearing and untested. Also pins that a schema refusal, which the dispatcher raises with zod prose before the bundle handler runs, reads as no code, and that both descriptors stay `require-result-or-throw` / `on-settle`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): dial the host on tap in the dev bundle row, and name it Opening Troubleshoot in a dev build acquired a client at mount, which is what kicks a dial, on a screen that opened no connection before. The probe now acquires only once the row is tapped, and each request owns its AbortController so a re-run, an unmount or StrictMode's second mount abandons the previous fetch and stops its chunk reads instead of holding the host's read slots. The screen carries no host parameter and troubleshoots every paired host, so there is no host it is "on": the row still takes the first paired host but now names it in the result instead of implying it speaks for all of them. The label says whether it is still connecting or already fetching. There is no `__DEV__`-conditional `require` idiom in this repo to trim the row out of a release bundle with, which the route now records. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): refresh the recorder corpus counts 397 scenarios, 787 goldens, 790 tests from the README's own three-file command. The 44 salvage goldens are unchanged; only the total they are quoted against moved. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin and re-record for the mid-asset stop Baseline moves to |
||
|
|
bfdec26352 |
fix(lint): enable anti-slop/no-object-parameters (#20781)
The rule rejects the broad `object` type on any function input (declarations, expressions, arrows, methods, call/construct signatures, function types), plus local aliases and unions that resolve to `object`. `object` accepts every non-primitive while exposing no properties, so it documents nothing and pushes callers into assertions at the boundary. Fixes all 185 violations across src, config, tests and mobile, and flips the rule from "off" to "error" in config/oxlint-anti-slop.json. Approach: replace each `object` input with the type its owner already has. Most sites took an existing domain type or a type-only import (36 added); 40 new aliases name shapes that had none. Where a value is genuinely only compared by reference, it gets a named identity token instead of a shape -- `Record<string, never>`, the built-in `WeakKey`, or a `unique symbol` brand, matching the branding already used in src/shared. Same treatment for WeakMap and Map key parameters. Two `as unknown as` casts became unnecessary once the parameter carried a real type and were removed; no new casts were added. Suppressions added: none. No `oxlint-disable` for this rule anywhere, and no max-lines disable or per-file bump. Three files sat exactly at their max-lines cap, so the added type imports were made line-neutral rather than suppressed: - src/main/ipc/browser.ts exports the existing guest-registration args type (renamed BrowserGuestArgs) so browser.test.ts reuses it on one line. - pane-scroll.ts takes TerminalScrollIntentTarget through the existing pane-manager-types import via a type-only re-export. - direct-rpc-client.ts drops the identity parameter entirely: the session check moved into the sendProbe callback that owns the token. Verified: anti-slop config reports zero violations over src config tests mobile; run-typecheck-projects-in-parallel exits 0; 144 affected test files pass (1749 tests); oxlint and oxfmt clean on all changed files. Mobile has no runnable test/typecheck target in this worktree (expo is not installed), so its 6 files were typechecked against a standalone config and diffed against the base branch -- error sets are byte-identical, including test files. |
||
|
|
9b2b02bb3b |
perf(mobile): reuse UTF-8 prefix truncation for diagnostics (#20358)
Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
341b13cf67 |
Restore mobile push and fix cold-start dismissals (#20068)
* Restore mobile push for delivery validation * fix(mobile): register push task before headless startup * Add authenticated mobile push test and fix iOS release entitlements * Mock push-test transport in notification consent tests * Fix slept workspace test for structured remount result * Fix mobile notification review findings * Pad Android notification icon to prevent square cropping * fix(mobile): present visible Android data pushes in foreground * test: use deterministic clock for teardown deadline * fix(mobile): present foreground pushes through Expo public APIs * fix(mobile): check push eligibility before foreground scheduling * fix(mobile): register push from shared host connection lifecycle |
||
|
|
4e1681338c | refactor(mobile): extract settings, diagnostics and editor-document screens from their routes (#19675) | ||
|
|
d936d8da82 |
revert(mobile): pull the relay connect-speed mobile pass pending a smaller, verified re-land (#19348)
* Revert "feat(mobile): time relay dial stages so diagnostics say where a slow connect went (#19245)" This reverts commit |
||
|
|
83b1558ecc |
feat(mobile): time relay dial stages so diagnostics say where a slow connect went (#19245)
* feat(mobile): time relay dial stages so diagnostics say where a slow connect went A 10s connect was unattributable from a shared report. Relay dial stages carried no timestamps, so nothing could tell "the cell never answered relay-hello" from "the E2EE handshake was slow", and the per-state dweltMs the client already computed went only to console.log — invisible without a debug build. RelayDialStageTracker now stamps each stage entry from a monotonic clock (performance.now where present, wall clock otherwise) and returns the duration of the stage it just left. The session logs one entry per stage, and settles the in-flight stage on connect, failure, or close, so a dial that dies mid-way still names the stage it never finished. dweltMs joins the same buffer as a structured field instead of console. Durations ride the existing per-host log buffer and its cap, so memory is unchanged and no new storage appears. The report derives two lines from them: the latest dial's stage breakdown (a reconnect loop must not average away the attempt being reported) and total dwell per connection state. Both are numbers and closed-enum names, and the entries still pass through the existing redaction. * fix(mobile): never let a diagnostics sink break a dial, and pin timing names to their enums Review follow-ups on the dial-stage timing work. The stage timing emitted on the confirm's success path ran inside the try that calls fail(), so an onLog sink that threw would have turned a good connect into a failed session. The same hazard existed on the direct path, where the dwell emit sits in publish() ahead of the listener loop and the connect waiters. Both sink calls are now isolated: a broken sink loses a log line and nothing else. The persisted-log validator accepted any string as a timing name, and the report echoes that name unredacted. Names are now checked against the closed enum for their kind, backed by Record<Union, true> tables so adding a stage or a state breaks the build rather than silently widening what a corrupted store can inject. Entry volume: every reconnect cycle walks four connection states, so logging each one would roughly double what a slow-connect report holds against the unchanged 200-entry per-host cap. Transitions under 100ms are therefore not buffered. They cannot be where a slow connect spent its time, and console still shows all of them. States that flap slowly, which is the case support cares about, still land in the log. RpcClientConnectionState takes an optional clock so dwell thresholds are testable without sleeping. * fix(mobile): reject a negative stored stage duration when hydrating the log A persisted timing only had to be finite to survive hydration, so a corrupted `ms: -1` reached the diagnostics report, where the dial summary sums the stage durations and a negative would subtract from the total. Producers clamp at 0 (`elapsedMs`), so anything below it is corruption. 0 itself still hydrates: a stage the dial passes through instantly is real. * refactor(mobile): move the relay liveness profile out of the session so the dial log fits * fix(mobile): never let the liveness-timeout log line keep a dead relay connected * test(mobile): prove the throwing timeout sink was actually reached |
||
|
|
5f75b247b9 |
Extract mobile troubleshoot screen styles (#17146)
* Split speech session lifecycle * Split terminal output scheduler pipeline * Split mobile browser pane modules * Prune resolved max-lines suppressions * Split pane tree equalization logic * Extract mobile troubleshoot screen styles * Fix F3-speech for #17123 * Fix F1-cycle for #17131 |
||
|
|
bf5660df51 | feat(mobile): add causal network diagnostics (#16837) | ||
|
|
5dc6799c48 |
Rename 'local network' to 'LAN' across the UI (#10216)
Improves clarity and consistency throughout mobile pairing, settings, and permission descriptions. Makes the distinction from Tailscale more explicit where relevant. Updates all translated locales. |
||
|
|
9c111fd7aa |
mobile: per-host connection log screen with copy-diagnostics (#7984)
The rpc-client has always emitted a detailed connection lifecycle log (dials, timeouts, close codes, handshake steps, retries) via onLog, but only the pairing screen wired it up — for long-lived host connections everything went to console.log, invisible to users. Debugging reports like #7824/#6928 meant asking reporters for facts the app already knew. - connection-log-buffer: bounded (200/host) module-level ring buffer with referentially-stable snapshots for useSyncExternalStore; survives client swaps and provider remounts. - client-context: wire onLog for every shared host client. - connection-log screen: live per-host log (reuses the pairing ConnectionLog component), host picker, and a Copy Diagnostics button that bundles app/platform versions, endpoint (flagged if Tailscale), state, attempt count, last-connected, and the event log into one shareable blob. - troubleshoot: 'View connection log' entry point. Co-authored-by: Orca <help@stably.ai> |
||
|
|
73f1bbfc94 |
mobile: recover from wedged Tailscale tunnels and say 'check Tailscale' when it's the likely culprit (#7980)
A wedged Tailscale tunnel (known iOS failure mode) produces no AppState or network-type transition, so no revival nudge ever fires and the reconnect loop parked permanently at its give-up cap — users had to toggle Tailscale off/on just to force a transition (#7824). - rpc-client: past the give-up cap, drop to a 90s trickle dial instead of parking so the session self-heals once the tunnel recovers. - host screen: nudge the shared client on focus so opening the host retries immediately instead of waiting out a backoff/trickle timer. - connection-health: warning/unreachable verdicts on 100.64/10 or *.ts.net endpoints now carry a 'check Tailscale' hint, shown on the home host list and the in-session status line after ~3 failed attempts. - troubleshoot: 'Cannot reach <tailnet-ip>' now says to check Tailscale, adds a dedicated Tailscale section, and stops telling Tailscale users to disable their VPN (that advice killed their only route to the host); sections extracted to troubleshoot-common-issues.tsx to stay under the max-lines cap. Co-authored-by: Orca <help@stably.ai> |
||
|
|
0bb62b9d29 | Add braces to mobile control flow (#4351) | ||
|
|
8db35593b5 | fix: handle malformed mobile diagnostic endpoints | ||
|
|
5c37ddfca8 | perf: clean mobile diagnostic fetch timeout (#4063) |