Commit Graph
1205 Commits
Author SHA1 Message Date
efcf8f0888 fix(mobile): keep terminal scroll gestures from becoming taps (#31445)
Fixes #25457
Fixes #25458

Supersedes community PR #25432, community PR #25435. Original contributor credit is preserved below.

Co-authored-by: Prateek Rungta <prungta2@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 01:16:49 -07:00
NeilandWooseong Kim 251bbf7bb0 fix(mobile): mount sessions with saved preferences (#31443)
Fixes #30517

Supersedes community PR #30518. Original contributor credit is preserved below.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-21 01:16:35 -07:00
NeilandiBakuman 11a1cc4d8b fix(mobile): load folder workspace agent presets on the correct host (#31442)
Fixes #16215

Supersedes community PR #16216. Original contributor credit is preserved below.

Co-authored-by: iBakuman <67883044+iBakuman@users.noreply.github.com>
2026-10-21 01:16:19 -07:00
NeilandWooseong Kim 7a2057e65c fix(mobile): retire old touch gestures before snapshot replay (#31437)
Fixes #30300

Supersedes community PR #30312. Original contributor credit is preserved below.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-21 01:16:03 -07:00
NeilandWooseong Kim 21322a56aa fix(mobile): cancel stale shortcut list drags (#31436)
Fixes #30302

Supersedes community PR #30314. Original contributor credit is preserved below.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-21 01:15:49 -07:00
NeilandWooseong Kim ea401d7164 fix(mobile): restore dictation after iOS audio interruptions (#31435)
Fixes #27498

Supersedes community PR #30297. Original contributor credit is preserved below.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-21 01:15:29 -07:00
NeilandWooseong Kim 0dbc6aa3c4 fix(mobile): select Unicode words at rendered cell boundaries (#31434)
Fixes #30301

Supersedes community PR #30325. Original contributor credit is preserved below.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-21 01:15:12 -07:00
NeilandWooseong Kim 8b9ba72c3b fix(mobile): retire stale terminal stream fits and callbacks (#31432)
Retire old terminal stream callbacks and delayed fits across reconnects, unsubscribe, terminal replacement, and synchronous termination. Preserve fresh output and the latest accepted fit.

Fixes #30505
Adapted from community PR #30506.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-21 00:31:18 -07:00
NeilandWooseong Kim 46b83a9529 fix(mobile): drag terminal selection with the long-press finger (#31433)
Let the finger that creates a long-press selection extend it through the existing selection-drag dispatcher. Verified with native iPhone touches and focused lifecycle tests.

Fixes #30554
Adapted from community PR #30555.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-21 00:30:43 -07:00
NeilandShahar Mor 45ee56e1a5 fix(mobile): preserve native iOS dictation placeholders (#31356)
Preserve UIKit recognition placeholders during equal-text Fabric synchronization and reset recognition state when the native input is recycled.

Adapted from community PR #26644 by Shahar Mor. Verified with matched source-built native Debug lifecycle probes for both input classes; physical spoken dictation remains in the final manual batch.

Co-authored-by: Shahar Mor <shaharmor1@gmail.com>
2026-10-20 23:52:49 -07:00
Brennan Benson 6de974ca5b Mobile chat: remove the Tools toggle and its open-everything mode (#31188)
* fix(mobile): remove the chat's Tools toggle and its open-everything mode

* test(mobile): keep the scroll-ownership tests' runs drawn without the Tools default

* test(mobile): the scroll-ownership harness opens settled turns as the old Tools default did
2026-10-20 22:56:11 -07:00
NeilandWooseong Kim 12e0b37293 fix(mobile): keep dictation controls in sync with host settings (#31349)
Honor enabled voice settings across terminal/chat controls and capture. Refresh hold/toggle state on connection, focus and foreground while fencing stale replies.

Adversarial review: 63 existing tests plus six disable/late-finish/unmount/settings ownership probes pass; final required CI and aggregate gate pass. Existing native screenshots and microphone lifecycle exercise are attached to the PR.

Fixes #30307. Fixes #30511.
Adapted from community #30316 and #30513 by @innocarpe.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-20 21:45:10 -07:00
fa35698db5 fix(mobile): keep keyboard accessory row height stable (#31363)
fix(mobile): keep keyboard accessory row height stable
Stretch the dismiss control within the accessory row so showing it no
longer adds height and needlessly reduces the terminal viewport.

Adapted from community PR #23511 by @innocarpe.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 21:32:47 -07:00
NeilandWooseong Kim f19ce3635b fix(mobile): show the bundled whale on DeepSeek Build tabs (#31359)
fix(mobile): reuse DeepSeek whale for Build tabs
Adapted from community PR #30525 with a focused component regression and native current-shell validation.

test(mobile): use checked icon renderer guard


test(mobile): keep icon diagnostic with review evidence
Native before/after captures cover this low-impact asset mapping; retain the local fail/pass diagnostic outside the product diff.

Use checked optional type for mobile icon lookup

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-20 21:32:39 -07:00
Neilandbbingz 3fd90e94e0 fix(mobile): preserve live palettes on identical themes (#31355)
fix(mobile): skip identical terminal theme assignments
Preserve live TUI palette changes and contrast caches on repeated normalized host themes. Keep explicit foreground recovery repaints and published desktop contrast overrides.

Co-authored-by: bbingz <zzb@gxsmjx.com>
2026-10-20 21:32:23 -07:00
NeilandWooseong Kim 3ed0bb4dfc fix(mobile): match home-indicator inset to terminal input panel (#31354)
fix(mobile): paint terminal dock bottom safe area
Adapted from community PR #30318.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-20 21:31:25 -07:00
NeilandWooseong Kim 6537842f0c fix(mobile): blend workspace header into status-bar safe area (#31353)
fix(mobile): match workspace header safe-area surface
Adapted from community PR #30531 with native iOS simulator before/after validation.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
2026-10-20 21:31:20 -07:00
Brennan Benson 03f4ce3d6b fix(mobile): show the session title beside the tabs on a landscape phone (#30879)
The served session page (react-native-web) reads flex: 0 as flex-basis 0%,
so the title block collapsed to zero width beside the tab strip. flex: -1
is natural width capped at 160, shrinking only when the row overflows, on
both web and native.
2026-10-20 20:59:19 -07:00
Neil fece03b5ac fix(orcad): voice dictation on headless orca serve hosts (#31217)
* fix(orcad): voice dictation on headless orca serve hosts

Mobile dictation against an orca serve host answered speech_unavailable
since serve moved to orcad: orcad never installed the speech service
factories, and the model manager and download imported Electron.

The model download now takes a host request opener (Chromium net on the
desktop, Node https on orcad, with redirects mapped to the same event),
the STT worker ships in the orcad slot, and orca serve hands orcad the
installed app's on-device recognizer. A host without a recognizer (an
SSH or WSL managed slot) lists only cloud models and sends a phone that
picked an on-device model to the setup sheet. Mobile names the fix when
an older server still has no speech at all.

The parity test now requires both roots to install speech.

Reported-by: delorean2020 (@delorean2020)

* fix(orcad): retry Node download drops, stage the recognizer on Windows, review fixes

- Node https errors (ECONNRESET, ETIMEDOUT, ...) are retryable, so an
  interrupted orcad model download resumes (Codex P2).
- On Windows orca serve hands orcad a per-version copy of the recognizer
  in the profile, so a loaded addon never locks the app's files against
  an update (Opus).
- Drop a recognizer candidate path that could never match; one SAFETY
  helper per test file instead of repeated casts; comments.

* fix(orcad): cloud speech is ready only with a key this host can decrypt; prune superseded recognizer copies

Codex round 2: a desktop-sealed OpenAI key exists on disk but orcad's
secret store cannot open it, so the catalog reported cloud models ready
and dictation failed at the end. orcad's model manager now proves the
key decrypts. The desktop keeps its existence check (no keychain prompt).

* fix(orcad): never prune staged recognizers before the profile lock; copy linked packages as real files

Codex round 3. ref-oss check: versioned copies are cleaned only by the
process that owns them, so the pre-lock prune is dropped rather than
patched.

* fix(orcad): remove a partial recognizer copy when staging fails (Codex round 4)

* fix(speech): never hand out another store's ciphertext as the OpenAI key (Codex round 5)

* refactor(orcad): no recognizer copy on Windows; Windows orcad keeps cloud dictation only

Design check after review round 5: every remaining finding came from the
Windows copy that dodges addon file locks. Dropping it shrinks the
surface to a path lookup; Linux and macOS serve keep on-device models.

* fix(speech): name the fix when an old server refuses a mic tap; always set the recognizer env; retry ENOTFOUND (final Opus review)

* fix(managed-server): probe install completeness relative to the slot dir

CI: shipping stt-worker.js added one more absolute slot path to the
install-complete probe, pushing it further past the exec-size budget.
Testing each artifact relative to the slot (inside a subshell cd) cuts
the probe from ~12.2 KB to ~3.6 KB, so it drops off the over-budget
list instead of raising a ceiling.

* fix(mobile): pin host.platform's read to a platform type, independent of which zod copy resolves

With mobile's own dependencies installed (which CI does for any PR that
touches mobile/), the e2e program resolves two zod copies, and the
salvaged platform arm from src/shared inferred as {}. The schema now
narrows through an explicit platform guard.

* fix(mobile): keep the host.platform read line intact for its RPC mutant; narrow in a second step

Mobile verify: the recorded-RPC mutant anchors on the read line, which
the previous guard rewrote, so the mutant could not apply.

* refactor(cli): app-executable resolution in its own module; drop the platform guard now zod is aligned

launch.ts went one line past max-lines with main; resolveAppRoot,
resolveForegroundOrcaExecutable, getExecutableAppArgs and
stripElectronRunAsNode move to orca-app-executable.ts (re-exported, so
callers are unchanged). With mobile and root on one zod (#31260), the
host.platform schema no longer needs the extra narrowing step.

* refactor(cli): import app-executable helpers from their module; no re-exports from launch.ts
2026-10-20 19:54:55 -07:00
Neil ecc4a6cacf fix(mobile): show embedded images in Markdown previews (#31304)
Render standalone embedded images in Markdown file previews and open them in the existing full-screen viewer. Resolve relative paths through authorized paired-host reads.

Thanks to @dudupii for reporting and diagnosing #24468 and implementing the broader solution in #24469; related editor integration is preserved in #25594. This independently implemented preview fix leaves editor image serialization, inline images, and pinch zoom as separate work.

Validation: focused tests, iPhone simulator proof, and all CI checks passed.
2026-10-20 19:38:23 -07:00
Neil a237b031df fix(mobile): open relative Markdown file links (#31298)
Open relative Markdown file links in the mobile file preview with workspace and execution-host scoping.

Thanks to @tomasklime for the report and diagnosis in #30368. Covers read-mode links; rich-editor links remain separate.

Validation: focused tests, iPhone simulator proof, and all CI checks passed.
2026-10-20 19:31:10 -07:00
Neil 513c24bc16 fix(mobile): preserve unsent terminal commands across workspace navigation (#31314)
Persist unsent buffered terminal commands per paired host, workspace, execution host, and surface across navigation. Preserve newer typing during storage hydration and clear successfully sent drafts.

Validation: draft/storage/bridge tests, signed iPhone simulator build, and all CI checks passed.
2026-10-20 19:31:05 -07:00
Neil 4f70d6cee5 fix(mobile): refresh staged files when returning from review (#31295)
Refresh Source Control when returning from review so staged files and commit availability reflect completed mutations.

Validation: focused loader tests, iPhone simulator proof, and all CI checks passed.
2026-10-20 19:31:01 -07:00
Neil a308277f9a fix(deps): root and mobile resolve one zod, and CI fails if they drift (#31260)
* fix(deps): root and mobile resolve one zod, and CI fails if they drift

Mobile type-checks src/shared zod schemas. With mobile on zod 4.6.5 and the
root on 4.6.7, TypeScript treated them as unrelated packages, so wrapped
schemas inferred {} (tests/e2e/helpers/phone-feed-list.ts:154) whenever a
job installed mobile deps.

* fix(deps): guard every package mobile's tsconfig remaps, not only zod

* test(ci): pin the shared-dependency version check to the static phase
2026-10-20 19:12:29 -07:00
Brennan Benson c93514db5b refactor(protocol): each area owns its published runtime capability list (#30641)
* refactor(protocol): each area owns its published runtime capability list

RUNTIME_CAPABILITIES now spreads one array per area instead of a flat
hand-appended list, and runtime-capability-constants.ts is split into
the areas that own each constant. The published set is unchanged and
pinned by an exact sorted test.

* test: preserve capability baseline while allowing additions
2026-10-20 17:12:19 -07:00
Brennan Benson f812c67a05 feat(native-chat): Codex chats list and run the skills Codex loaded (#30937)
* feat(native-chat): Codex chats list and run the skills Codex loaded

A Codex chat's skill list now comes from Codex itself (`skills/list`), live
while the chat runs and refreshed when Codex says its skills changed, and
from a short-lived Codex check while the chat is at rest. Plugin skills keep
Codex's `plugin:skill` names, disabled skills drop out, and each skill file
is its own row. A skill picked from the list reaches Codex as that exact
skill, beside the unchanged `$name` text, on hosts that take it.

* test(native-chat): account for Codex's skill list call; send without a trailing argument

* fix: align skill probes with chat launches and preserve picked mentions

* fix: distinguish skill choices in phone accessibility labels

* fix: clear picked skill files when queued text changes

* fix(native-chat): refresh resting commands when launch settings change

* fix: keep WSL agent refresh on its execution host

* fix: preserve skill selection at read and send boundaries

* fix: narrow agent identity for selected skill notice

* fix: use checked agent predicate for skill notice label

* test: retain locale in composer translation mocks
2026-10-20 15:33:07 -07:00
Neil 749aadcbc8 fix(mobile): pick a cloud dictation model from the phone (#31221)
* fix(mobile): pick a cloud dictation model from the phone

The dictation setup sheet and Voice settings showed OpenAI models as
read-only text, so a host that lists only cloud models (Windows orca
serve, managed SSH or WSL servers) left the phone with nothing to pick.
A ready cloud row is now selectable through the same speech.dictation.setup
modelId write as a downloaded model; a cloud row without a key says to add
it on the paired computer instead of "Set up on desktop".

Refs #31174
Reported-by: delorean2020 (@delorean2020)

* style(mobile): template literal on the re-indented delete label

* test(mobile): complete the setup fixtures for the tests typecheck ratchet (Codex)

* fix(mobile): short not-ready label that cannot squeeze the model name; button roles and labels
2026-10-20 14:41:09 -07:00
Jinwoo Hong fb21729157 chore(mobile): bump to 0.0.58 and Android versionCode 22 (#31194)
Matches the iOS 0.0.58 build.
2026-10-20 15:45:03 -04:00
Brennan Benson ad1bcfe502 A chat's model, effort and Fast belong to the chat, not to a model (#30561)
* fix(native-chat): a chat's model, effort and Fast belong to the chat, not to a model

Structured chats kept their option values per model on the client, so a value
the host saved without a model (a resumed chat that never picked one, Codex
Fast saved on the conversation) had nowhere to live. The client either dropped
it or labelled the chat with the listing's default model to hold it.

- One flat record per chat holds model, effort, speed and Fast independently;
  a model pick changes only the model and the host reconciles the rest.
- Controls come from the model they apply to, separately from the values; with
  no model known, a saved Codex speed still shows and Standard turns it off.
- The options answer marks values a catalog default supplied
  (current.defaulted). A resumed chat never names or sends such a model as its
  own; a new chat still shows the default the host says it runs. An answer
  without the mark reads as before.
- Every send, including a new chat's first message, carries the chat's own
  model and effort and held picks, never a default read off the picker.
- A held launch pick supersedes only an earlier pick of the same option.

Tracked in STA-10947.

* fix(native-chat): a new chat's first send still pins the default model it shows

A new chat that never picked a model carries the default its picker names, as
before, so it keeps the model it actually ran after the CLI's default changes.
Only a resumed chat's defaulted model is never named or sent.

Tracked in STA-10947.

* fix(native-chat): narrow the host catalog before reading whether its listing names the default

The flag lives only on a listed catalog, so the check confirms the catalog is not the unknown
answer before reading it; the first-frame test reads the chat's flat model value.

* fix(native-chat): a reopened chat whose saved model was refused records the model it now runs

After a retired saved model is reset, the options answer no longer marks the model the child runs
as a default, so the next send records it instead of replaying the refused model on every reopen.

* fix(native-chat): a new chat names and pins the default it shows, and a model pick keeps only what the model offers

A chat this view started names the default model the host fills in, whichever of the model list and
the options answer lands first, and its first send carries the shown model and effort. A resumed
chat still names and sends only its own values. A model pick, held or live, keeps an effort, speed
or Fast only where the new model offers it, so a value the model cannot take is never shown, sent
or remembered.

* test(native-chat): give the no-Fast model fixture its required default flag

* fix(native-chat): an open chat's model pick leaves reconciling to the host

A live pick no longer drops values on the client; the host's reply, when it carries its full
value set, replaces the chat's own values as a read does. Only a pick held before launch, with no
host yet, keeps just what the picked model offers.

* fix(native-chat): a pick reply only adds the values it names

A pick's reply carries only the values the host set or was asked for, so it can't speak for the
rest: it no longer removes values it leaves out. Joined tests pin real reply shapes and a new
Claude chat in a fresh Claude home naming and pinning Opus High.

* fix(native-chat): a tier the drawn model doesn't offer shows as Standard

A Speed value another listed model offers but the named model doesn't is drawn as Standard, which
is what the host reconciles it to, while the chat's record keeps the host's value. Effort, a tier
no listed model offers, an unlisted model and a stand-in model's controls still show what the host
holds. After a lost pick reply the picker no longer shows the old tier as a raw extra choice.

* fix(native-chat): a never-sent chat names and pins its default after a reopen

The options answer no longer marks the default model and effort as stand-ins for a chat this host
started that has sent nothing, so a chat reopened after a restart or on the phone names them and
its first send pins them, as before. A chat taken from history, or one that has sent, keeps the mark.

* fix(native-chat): narrow the missing record as null and type the new-chat fixture's handle chain

* test(native-chat): the unloaded-turn context test's record store answers null for a missing record

* fix(native-chat): a chat's speed shows what its next turn runs

A Codex tier the drawn model lacks shows as Standard, which is how Codex runs it, except Flex, which
Codex passes regardless: established Flex keeps showing, and after a model pick the host hasn't
answered it shows no selection until the reply or a read. Each Codex turn sends the same effective
tier without rewriting the chat's saved one, and a new chat's create carries held picks as the
picker draws them, settling them so they aren't applied again after publish.

* fix(native-chat): one speed rule for picker, host and turns; never-sent needs proof; create keeps held picks

A Codex tier the model doesn't list shows and runs as Standard under one rule shared by the picker,
the host's live reconciliation, each turn and the at-rest answer, which keeps the saved tier for a
model that offers it. An older host's Flex shows as it reports it. A chat counts as never sent only
when no turn, rewind or untrustworthy journal says otherwise. A new chat's create carries held picks
as the picker draws them but keeps them as the user made them until create succeeds, so a refused
create and retry still has the user's own pick, and a newer pick still applies after publish.

* test(native-chat): type the resting journal mock and cut overlapping option tests

One typed resting-journal helper replaces six inline mocks and the broad object parameters the
low-evidence check rejects. Cuts tests the joined flows already pin: the resumed list-or-answer
ordering test, the restart-after-first-send view test, the held effort before a model pick, and
the model-pick and restart loop of the saved-Fast test.

* test(native-chat): cut option assertions the joined flows already pin

Drops the Codex Standard row (the joined tier test pins it), the dispatched-source and literal
effort-choice checks, and the resumed view test restart and model-pick steps; folds the pick table.

* fix(native-chat): an at-rest pick reply and answer name the speed the next start runs

The reply to a pick made at rest now carries the tier the chat model runs, through the same rule as
the at-rest answer, so an older app whose follow-up read fails shows what the next turn sends; the
saved record keeps the pick. A saved tier no listed model offers (a Flex from the user Codex config)
is the agent configured tier at rest too, as the live rule and the picker already read it. Restores
the pin that an accepted Codex reply naming Standard shows with no later read.

* fix(native-chat): a reopened chat with no saved model names its default at once

A reopened chat that never saved a model showed just "Model" until a send started its agent, while
main names the list default straight away. The picker now names the default the host marks for every
chat, as main does. Whether a send carries that default is a separate rule: a new chat sends it, so
its first send pins it; a reopened chat does not, so its next start still runs the agent's own.

* fix(native-chat): a chat with no saved model keeps the model its first turn ran

A chat that never saved a model followed whatever the agent's default was at each start, so a chat
resumed from history or a terminal could quietly switch models after a restart. When a message is
handed to the agent on such a chat, the host now saves the model that turn ran: the message's own
model when it carried one, otherwise the model the agent reports running. A default the host filled
in, no model at all, or a model the agent would refuse as a choice on later sends, is never saved. The write sets the model only while none is saved, inside
the record store's transaction, so a pick made meanwhile always wins and effort, speed and Fast are
untouched. One host step covers Claude, Codex, ACP agents and Pi. No new saved field.

* fix(native-chat): a chat with no saved choices keeps the model and effort its first turn ran

A Codex message that left the model or effort unset was filled from the model list's defaults, not
from what the running thread uses. Once the host saved the model a chat ran, the next Codex turn
carried that model alone and ran the list's default effort for it (a configured xhigh became
medium), and a message carrying only an effort ran the list's default model while the chat saved
the thread's. A Codex message now resolves an unset model or effort to the thread's own, and only a
switch to a different model takes that model's default effort, the same rule the other agents use.

When a chat with no saved model records what its first turn ran, it now records the effort, service
tier and Fast that turn ran too, in the same one-time store write: each only where the chat has none
saved, only from the message's own value or the agent's real report, never a filled-in default, so
a later change to the agent's configured effort no longer pairs the kept model with an effort it
never ran. No new saved field and no wire change.

A Codex follow-up sent while a chat's first turn runs is compared with what that turn actually runs,
so it steers instead of waiting for a new turn just because the host saved the model meanwhile.

Also fixes the delivery loop's optional recording hook type.

* fix(native-chat): key what a child ran by its provider child type

* fix(native-chat): record each option a turn ran where none is saved, and drop what a model pick can't run

A Codex message that only switched the tier set the thread's effort as a lasting override, so a later
switch to a model with no listed efforts was refused; the effort is again written only where the
model brought one or an earlier override needs replacing.

Recording what a turn ran now works per option: a chat that already saved its model still records
the effort it ran, and so on for each option it has none saved for. Tier and Fast come only from the
agent's own report, since a message steered into a running turn runs that turn's, and Fast is kept
only when the agent reports it actually routed on: an off state may only mean Fast is unavailable.

A model pick drops a saved effort, or Fast, that the newly picked model's listed row does not offer,
in the one host step both resting and live picks go through, so no later send carries a choice the
agent would refuse. The saved tier is kept, as before: it runs as the model's effective tier.

* fix(native-chat): save only options a turn really ran, and only ones the chat's model runs

A chat with a saved model recorded the effort its agent's settings named, not the one the turn
ran: with "effortLevel": "high" in Claude's settings and Haiku saved (no effort control), the chat
saved high and every later send was refused. A Codex thread effort its saved model doesn't list
did the same.

What a turn ran now comes from the agent's own dispatch: Claude reports the choices it applied and
the effort the CLI resolved for the model it applies, never the settings level alone; Codex reports
its final request over the thread's own model and effort, and a steer reports its running turn.
This evidence lives only in memory on the dispatch result; agents without it keep using their own
reports.

One admission rule now decides every saved option, inside the ordered record write: under a model
the saved catalog lists, an effort must be one it offers and Fast must be one it replays (Claude
needs positive support). A late record of a turn that ran on another model than the one the chat
now has saves none of that turn's options. A model pick drops inherited options its new model
doesn't run, an explicit pick or a new chat's option the model doesn't offer is refused with the
existing option refusal, and unknown catalogs or unlisted models pass as before. The saved tier
keeps its effective-tier rule, and nothing already saved is cleaned up later.

* fix(native-chat): an attach that founds a chat admits its options against the saved catalog too

* fix(native-chat): Claude and Codex always name what a turn ran, and picks are judged inside the write

The handover type was misnamed, which broke the typecheck. Claude and Codex now attach their
dispatch evidence even when it names nothing, so a Claude turn dispatched before its settings
readback never records the settings effort it did not run; the agent's own report still names the
model and stands in only for agents without dispatch evidence. Recording leaves the saved-model
and set-if-unset checks to the store's one admission rule. An explicit effort or Fast pick is now
checked inside the ordered write, with the same refusal, and only at rest: a running chat's own
menu can offer more than its saved catalog row (Pi's probe menu stops at high).

* test(native-chat): Codex admission tests accept the dispatch's run evidence

* fix(native-chat): a new chat's options are admitted against the folder list its picker showed

* fix(native-chat): a new chat's admission reads the list its picker read, resolved once by the create

* chore: re-run CI against main's formatting fix (#30918)

* chore: re-run CI against main's formatting fix (#30948)

* test(native-chat): explain the seed-options stub host cast

* fix(native-chat): attach keeps its two-argument call when there are no attach options

* Revert "fix(native-chat): attach keeps its two-argument call when there are no attach options"

This reverts commit e633d2065b.

* test(native-chat): create-path attach assertions check the params argument, not the arity
2026-10-20 11:55:34 -07:00
Brennan Benson 38034167e7 fix(mobile): keep the chat composer's controls inside its row (#28983)
* fix(mobile): keep the chat composer's controls inside its row

The model pill never shrank (React Native items default to flexShrink 0), so a
long model name slid under Mic and Send. The permission pill now shows only its
mode icon, resolved from one shared mode->icon-name table that desktop uses too.

* fix(mobile): resolve the permission icon only when a pill renders

* style: format the permission pill test

* fix(desktop): keep the permission icons a static record
2026-10-20 11:44:35 -07:00
Brennan Benson c88e9d7054 fix(mobile): reopen the phone chat's live feed from its last row when it ends (#29007)
* fix(mobile): reopen the phone chat's live feed from its last row when it ends

The phone opened a chat's live transcript stream once per connection and never reopened it when the
stream ended or failed while the connection stayed up, so the chat silently froze on old rows. Move
desktop's reopen-from-cursor read into src/shared and run the phone on it too, with an app-foreground
retry.

* fix(mobile): retry the chat feed on the app-resume signal client-context already relays

Module mocking react-native in the shared vitest setup is rejected; publish the existing revival
nudge instead, the way desktop's host-contact signal feeds its read.

* test(mobile): let the phone's coalesced live read apply each frame before comparing
2026-10-20 11:43:42 -07:00
Brennan Benson 6c7a6ddd54 fix(native-chat): save every chat reply and tool output whole; bound only the copy sent to clients (#29982)
* fix(native-chat): save every chat reply and tool output whole; bound only the copy sent to clients

Orca cut Codex replies, every agent's reasoning, plans, prompts and all tool
output to a 16 KB head when it saved them, and threw the rest away. The
limit existed so a reconnecting client is not replayed a huge row, but it
was applied at save time, so every transport limit became permanent data
loss.

The journal now keeps what the agent said. One outbound form, applied to
every history page, live batch and catch-up, sends a client the same 16 KB
head of each tool output and diff it always got, and marks the item
`shortened`. A new capability-gated `agentSession.item` RPC returns the
whole item, and the desktop and phone show "Show full output" / "Show full
message" when the copy was cut. Items past half a page are shortened and
marked instead of replaced with "too large to show".

Two bounds stay, both named: a 4 MiB ceiling per text (head and tail),
applied where every provider's rows enter the write queue so one write can
never stall a chat, and a preview on Grok/OpenCode/OMP/Pi calls while they
run, whose closing row is built from the whole value the translator held.

* test(native-chat): an over-budget history item is shortened and marked, not replaced

* fix(native-chat): format, keep the sink and projection under the line limit, update method count and tests

* fix(chat): preserve complete writes and keep detail current

* test(chat): align final tail reservation with captured content

* Prototype source-batch journal ownership for Claude

* Pin the resolved iterator promise in ownership experiment

* Revert "Pin the resolved iterator promise in ownership experiment"

This reverts commit bdb49ab738.

* Revert "Prototype source-batch journal ownership for Claude"

This reverts commit e5ed387f0c.

* Preserve large received chat content while pacing input

* Verify received chat writes wait for journal persistence

* Close test journal databases before removing temporary roots

* Exercise item detail in the cross-version chat wire manifest

* Pin complete saved output and fitted client publication

* Exercise Pi exit retention with real journal write pressure

* Settle terminal web test unmount before DOM teardown

* Reuse existing readers and contract support in session tests

* Reuse measured chat item sizes in history pages

* fix(chat): derive page form type from reader contract

* fix(chat): open phone user messages while loading full text

* test(native-chat): reuse imperative context provider construction

* test(native-chat): format full-content disclosure fixture

* ci: preserve unit shard diagnostics after a timeout

* ci: retain unit shard output when tests time out

* ci: expose unit shard watchdog and output stages

* Revert "ci: expose unit shard watchdog and output stages"

This reverts commit 0b4991b144.

* Revert "ci: retain unit shard output when tests time out"

This reverts commit 87eec4c04f.

* Revert "ci: preserve unit shard diagnostics after a timeout"

This reverts commit 1888c656bb.

* test: verify complete Claude tool output without large failure diffs

* Simplify reply extraction in cross-version session test

* Restore full message recovery for grouped phone thoughts

* Target recovery button in grouped thought fixture

* test(native-chat): count agentSession.item alongside main's new structured method

* fix(chat): reuse strict session subscription schema

* test(chat): open Codex before checking received content
2026-10-20 11:13:48 -07:00
Kelvin Amoaba 703b3134d7 fix(mobile): keep session tab icons full size beside long titles (#31169) 2026-10-20 17:44:58 +00:00
Kelvin Amoaba fbc7488276 perf(mobile): prepare the worktree checkout while the new-workspace form is open (#31167) 2026-10-20 17:44:30 +00:00
Brennan Benson d406e0c8ab feat(native-chat): edit a queued message's images on desktop: remove, paste, drop or attach (#30815)
* feat(native-chat): a queued message's Save can replace its images (host, wire and capability)

* feat(native-chat): edit a queued message's images on desktop: remove, attach, paste or drop

* refactor(native-chat): share the queued edit's image rules and drop the rarely-shown Save reasons

* fix(native-chat): a queued edit's refusal says the edit wasn't saved, and its wire takes a full image set

An expired upload refused on Save read "The command didn't run": the queued
message update fell through to the command write kind. It now has its own
kind whose sentence is "Your edit wasn't saved." The images param caps at
MAX_BLOCKS, matching the shared edit rule that counts them with the text.

* fix(native-chat): a queued edit keeps image markers right and owns drops and pastes on its card

- Removing a kept image drops its [Image #k] marker from the edit's text and
  renumbers the later ones (shared rule, reused by the phone), so the agent
  never reads a marker that names the wrong image.
- A file dragged from Orca's own tree onto the editing card goes to the edit,
  like a Finder/Explorer drop; the chat box overlay no longer covers the card.
- Cmd/Ctrl+V with focus on the card's buttons goes to the edit, not the chat box.
- A "changed" answer says "Your edit is in the chat box" only when something
  went there; otherwise it says the edit wasn't saved.
- The empty hint counts an uploading image as content and is not shown for
  agents without image input; the list grows only while thumbnails show.
- Every hand-off releases the edit's preview URLs.

* fix(native-chat): an in-app drop on the editing card goes through the chat box's owner check

The edit no longer runs its own copy of the in-app drop check, which resolved
the target host through a path that can read the Active Server setting. The
chat box's claim checks the drop and routes it by the chat's owner as before,
then hands the files to the editing card's sink when the drop landed on it.
Also: the kept-image id list is readonly where it is read, and the marker
rule looks up the remaining images in a Set.

* fix(native-chat): only an edit that takes images keeps its card's pastes, and no drop joins a Save in flight

The pane paste bridge no longer skips every paste inside a queued editor: the
card's own paste handler already claims image and file pastes, which the bridge
respects. A menu paste is left to the card only when the edit takes images (the
card is a drop owner), so an older host or an agent without image input behaves
as before, and text pasted with focus on the card's buttons goes to the chat box.
An in-app drop handed to the card while its Save is in flight adds nothing.

* fix(native-chat): a failed capability re-check keeps the host's last answer, and an edit takes no files before its lease

A re-check after regained contact that failed wrote "unknown" over a known
answer for the same host, so features the host had reported (such as editing
a queued message's images) vanished on a connection blip, mid-edit. A failed
check now keeps the last answer; a different host still starts at unknown.
Paste, Attach and OS or in-app drops into a queued edit are now blocked while
it acquires its lease, as typing already is.

* fix(native-chat): an image change is never saved as text alone when the host stops reading as taking images

Reverts the shared capability hook to keep main's behavior: a failed re-check
reads as unknown. Instead the queued edit derives from live state that an edit
whose image set changed can't be saved while the host doesn't read as taking
images, so a connection blip never saves renumbered markers over the old
images or drops added ones. Save comes back once the host answers again.

* fix(native-chat): keep queued image markers and drop feedback in sync

* fix(native-chat): derive queued image markers without changing edit text

* test(native-chat): the pane drop test's intake append returns the paths it didn't take

* fix(native-chat): route menu paste to the focused queued edit

* fix(native-chat): honor queued edit paste ownership before focus redirection

* fix(native-chat): preserve native paste without clipboard access

* fix(native-chat): keep paste in the owning queued edit
2026-10-20 10:35:09 -07:00
Jinwoo HongandE2E Test b887483e55 fix(sidebar): the phone reads projects and project groups from main's sidebar feed, installed with the rows (#31102)
* fix(sidebar): the phone reads projects and project groups from main's sidebar feed, installed with the rows

The phone built its project sections from a catalog the desktop window published through a
side channel (mobileRelay.projectSections). That copy changed on a timer after the event that told
the phone to re-read it, and a re-read asked for during a read in flight was dropped, so the phone
could keep an old grouping (a project under the wrong group, a group missing) indefinitely.

Main now publishes the catalog (repos with their group and order, project groups, projects and
host setups, every host's including paired servers') as a projectCatalog domain of the sidebar
feed, from its own store and its own reads of each paired server. The phone reads it from the same
install as the rows. mobileRelay.projectSections stays, answered from the same builder, for pages
that predate the domain; a page on a desktop without the domain still reads it, now re-reading
when an invalidation lands mid-read. The phone e2e list helper runs the page's real refresh
lifecycle instead of re-reading on every assertion, and covers a group move while a read is held.

* test(mobile): stand AppState in for the screen hooks' react-native mocks, and tolerate a world with no project rows in the parity catalog

* fix(sidebar): main files a server's copy of a grouped project under its group, and reads a server's catalog only on catalog events

- The window gives an ungrouped paired-server repo the group of an existing copy of the same project
  (applyInheritedProjectGroups). That function now lives in src/shared and main's catalog builder
  folds each server's repos through it, so the phone files the copy where the desktop does.
- A paired server's change watch now says whether the event can carry a catalog change. Only
  ready, a replay and reposChanged mark the catalog stale; worktreesChanged (pins, unread, sort
  stamps) reads rows only.
- The parity fixture now builds the server side from raw server answers, folded through the
  window's mergeFetchedReposForHost and through main's own server read and builder, with a server
  copy of a grouped project and a partial saved manual order. It fails without the inheritance.

* fix(sidebar): parse a paired server's catalog rows with a schema instead of reading keys reflectively

* fix(mobile): sync the feed ref after render and re-read queued project sections without an await loop

* fix(mobile): spell the AppState shape the refresh reads, so the e2e helper pulls in no React Native types

* test(sidebar): give the merged builder test's feed domains a project catalog

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 09:34:02 -04:00
94e55a2f3d fix(mobile): phone list resends lost option changes, trusts the feed's row facts, and retries a failed feed read (#31088)
* fix(mobile): resend lost Workspace option changes on reconnect, draw feed-owned row fields from the feed, retry a failed feed read

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(mobile): end a feed retry streak only on install, draw the feed's comment

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-20 06:47:40 -04:00
NeilandKAPUIST fadcb8a988 fix(sidebar): keep project names short and show their machine (#28250)
Keep same-name projects short across execution hosts and show their machine beside host-owned headers, including when collapsed. Reuse existing host identities and filtered section membership; retain global ownership for shared projects.

Includes tooltip, narrow-sidebar, alias and filtering regression fixes with visual proof in the PR.

Co-authored-by: KAPUIST <thsxornjs12@gmail.com>
2026-10-20 00:25:15 -07:00
Kelvin Amoaba 99379d1458 fix(mobile): let an add or removal replace corrupt stored terminal keys (#30977) 2026-10-20 03:00:27 +00:00
Kelvin Amoaba b2e678a391 fix(native-chat): keep /model, /effort, /clear and /compact in the / menu after an agent reports its commands (#30956) 2026-10-20 02:09:30 +00:00
Kelvin Amoaba 883583b4c8 fix(native-chat): hide history before /clear (#30902) 2026-10-20 01:09:38 +00:00
Wooseong KimandNeil f3a1238a34 perf(mobile): shorten output batching during terminal scroll gestures (#30319)
Co-authored-by: Neil <neil@stably.ai>
2026-10-19 16:15:24 -07:00
Brennan Benson 4a33cd8e73 fix(native-chat): a usage limit reads as a pause with its reset time and a Continue, not a failure (#30396)
* fix(native-chat): a usage limit reads as a pause with its reset time and a Continue, not a failure

Claude, Codex and Grok chats that hit the account's usage limit now end the
turn on one warning row that names the limit and, in the reader's clock,
when it resets, with Continue to pick the turn back up. Each adapter reads
its protocol's typed limit signal into a `usageLimitReached` failure fact;
older clients keep the host's sentence.

* fix(native-chat): keep the failure words and chat session files within their line budgets

Moves the attachment-problem sentences to their own file and hands the composer's send straight to Continue.

* fix(native-chat): drop a check the shown branch already rules out

* fix(i18n): ship the usage-limit failure words in the runtime catalog

* fix(native-chat): reconcile usage failures by owning turn and typed cause

* fix(codex): keep snapshot import type-only

* fix(claude): open refusal turns without opening empty placeholders

* fix(native-chat): bind usage Continue to the latest idle failed turn

* test(native-chat): isolate stale Continue from ordinary delivery progress

* test(native-chat): use a braced journal assertion

* test(native-chat): build failure rows through their word factory

* fix(claude): retain terminal failure evidence through result reconciliation

* fix(native-chat): complete usage Continue through the composer sender

* test(native-chat): provide the composer options surface in Continue coverage

* test(native-chat): use typed failure words in renderer fixtures

* test(native-chat): match the remote send payload apart from transport options

* fix(native-chat): replace the previous sender notice on Continue

* fix(native-chat): clear superseded Continue notices before rendering

* fix(native-chat): preserve ordinary composer send arguments

* fix(claude): resolve terminal ownership and preserve resultless quota failures

* fix(claude): keep repeated terminal detail unchanged and complete test fixtures

* fix(claude): resolve deferred terminal replays after owning journal writes

* refactor(native-chat): share tombstone append within the sink line budget

* fix(claude): retain both input and echo correlation on the live turn

* fix(claude): preserve unowned result completion after terminal resolution

* fix(claude): allow unassociated successful goal completion

* fix(claude): keep terminal completion on the live turn

* fix(claude): keep typed notices separate from quota failures

* fix(claude): preserve context-limit failures as ordinary errors

* fix(native-chat): keep quota failures visible in collapsed turns

* fix(native-chat): return keyboard Continue focus to the composer

* test(native-chat): keep quota fixture context stable

* test(native-chat): hoist quota context to module scope

* fix(i18n): use one Chinese word for the agent in both Continue tooltips
2026-10-19 14:43:16 -07:00
b0c55557f0 Keep phone chat history in one window while preserving reading position (#29271)
* fix(mobile): phone page loads older history only from the Load earlier tap

On the phone page a scroll near the top paged in older history, often while iOS
was still bouncing the list past the top. The place-hold then wrote scrollTop
while WebKit owned the scroll, and the list stopped scrolling until the reader
left the chat. The page now loads older history only from the existing Load
earlier control (the native app keeps its drag paging), and the place-hold
never writes while the list is bounced past either end.

* fix(mobile): a reconnect no longer drops the older page the reader asked for

A replayed snapshot cancelled any in-flight "Load earlier messages" page, even
when the replay merged onto the same window and cursor, and every other lost
page (host refusal, error reply, transport drop) was swallowed, so the control
went back to its label as if nothing had been tried.

A page now lands unless its window was replaced or the cursor it was sent from
changed. A page that is lost shows one line under the control,
"Couldn't load earlier messages. Tap to try again.", cleared by the next tap or
a page that lands.

* fix(mobile): keep the failed-page copy out of raw JSX text

* fix(mobile): name the failed-page copy

* fix(mobile): clear the failed-page line when the window is replaced

* docs(mobile): the older-page read's comment names the retry line a refusal now shows

* feat(mobile): one older-page loader that owns the spinner, the budget and the outcome

* fix(mobile): terminal-backed chats read older pages through the shared loader, bounded to one tap's budget

* fix(mobile): agent-session chats read older pages through the shared loader

A failed page now shows the retry line instead of an invisible chat error,
history that can never be read ends paging, and one tap shares one budget.

* fix(mobile): the Load earlier control renders the loader's outcome for both chat types

A retryable failure keeps the button with the retry line; history that can never
be read swaps the button for one line saying so; scrolling at the top no longer
re-fires a failed read.

* style(mobile): format the read-back guard

* test(mobile): a failed agent-session older page keeps the chat readable, retries on a tap, and ends at unreadable history

* test(mobile): the older-page read now records its per-tap budget in the page goldens

Hand-updated: only the nativeChat.readSession options change (absent -> 15 s
budget spanning connect); every value is re-hashed and re-keyed as the recorder does.

* fix(mobile): every history reset ends an agent-session tap quietly, as the subscription's reload replaces the window

Only a final read refusal (damaged history, or saved by a newer Orca) ends a tap at
'Earlier messages can't be loaded.'; the test now drives that reachable path.

* fix(mobile): history that can't be loaded stays a line until the window is replaced

* fix(mobile): a page row keeps its key when older messages join it

A folded row's id is its first message's, so an older page that brings the
start of a tool run (or an older sibling of a roster) replaced the displayed
row: the list remounted it and the page's history split lost its first row.
Rows now carry a presentation key that stays while any of their messages
draw, handed on from the last committed render by the raw message ids each
row draws, and a vanished key resolves to the row that took its messages.

* fix(mobile): the page holds a parked reader's message, and leaves a following one to the follow owner

The place-hold fired on the first drawn row's id, which a kept row key no
longer changes, and held the lowest row on screen even while the reader
followed the end, so at the 2,000 cap it wrote against the follow owner.
It now fires when the source window's first message changes (an older page,
a cap trim), only for a reader who is not following, and puts back the
first tagged row on screen, else the nearest row the update kept, through
the row keys. iOS bounce still gets no write.

* fix(mobile): an open tool run keeps the reader's lines when older calls join it

With the run's row kept, older calls joining its front pushed the lines the
reader was on behind Show more. The reveal now counts the lines added above
its first line apart from what Show more asked for, so those lines stay
shown, each diff keeps its share, and Show less only undoes Show more. Lines
and the run are tagged, so the page's hold puts the line being read back
where it was. The flow test opens a chat inside a silent run, loads the rest
from an old host's cursorless reply, then lands a reply and a send.

* fix(mobile): an open tool run reads its first line from the pairs it already drew

Pairing the run again for its first line on every render broke the bounded
pairing a 2,049-call run relies on (one pairing per reveal). The flow test's
icon mock names every icon the run reads.

* fix(mobile): a cap trim that splits the top run leaves the run its key

Row-key hand-on walked the current rows oldest first, so when a trim left a
run's first result alone above it, that orphan took the run's key and the open
run remounted closed. Walk newest row first so the run keeps it.

* fix(mobile): a row's key belongs to the messages it is made of, not to the results it shows

A cap trim that split the top run gave the run's key to whichever split-off result row a
position-ordered scan met first: the result above it for calls made one at a time, the one
below it for parallel calls. The run then remounted closed and the reader's line went.

Each row is now identified by its own sources (a run by its assistant and call messages, a
roster by its group, a lone result row by its results) and keeps the key of the one previous
row it continues; results are content only. A key no row kept resolves to the row that took
in what it drew. If identity ever came from two rows, neither inherits.

* fix(mobile): the page holds the first part of what the reader saw that is still the same part

The hold took a line only from the first row on screen and otherwise trusted
that row's container. A result row absorbed by its run, a roster the fold moved
to an older sibling, or a trimmed line then moved the reader. It now lists the
tool lines on screen across every row (a run header only when no line shows,
a row top only when neither), and puts back the first one still drawn: a run
header only in the row that kept its key, a row top only while the same
message places that row. One write, skipped under half a pixel.

* test(mobile): pin the run-header rule in the reading-place flow; fold the hold's last cases into it

An open run whose header is above the screen holds nothing: with only its
Show more on screen, the rows after it keep their height when older calls
join. Removing the header-on-screen check moves them 840 px. The collapsed
case now checks the same rows. The iOS bounce and native cases move into
the flow test over its page model, so the hold's separate fake scroller
goes.

* fix(mobile): the page's follow owner pins to the live end before paint when the transcript grows

RN Web reports content size a task after layout, so with no browser scroll
anchoring a following reader saw one frame far above the end after Load earlier.
One ResizeObserver over the page scroller and its content, bound to the list's
mount, calls the same pin, which reads the live end and writes only when short of it.

* test(mobile): one page follow flow pins growth, the late size report and layout to the live end

The page's follow tests fold into one flow so the view test stays within its
line limit; the place-hold reads the follow owner through one prop bag so the
view does too.

* fix(mobile): only the page asks the list for its scrollable node

The native list never needs the resize observer, so it shouldn't reach for
the page-only node at attach.

* fix(mobile): a following reader on the page keeps the end as the browser's scroll anchor

At the 2,000-message cap a trim plus an append of the same height changes no size, so the
follow owner hears nothing, and Chromium's scroll anchoring keeps a visible message still,
scrolling the reader one message off the end each time. While the reader follows, the page's
scroller now takes the transcript out of anchoring and leaves a 1 px end marker as the anchor.
Parked readers and native lists are unchanged.

* test(mobile): move the cap-anchoring flow into its own view test file

MobileNativeChatView.test.ts is at the 800-line limit for test files.

* fix(mobile): the cap-anchoring test passes every required view prop; the style augmentation names why it is an interface

* fix(mobile): type the page's anchoring style locally instead of augmenting ViewStyle

* style(mobile): wrap the page scroller's styles

* test(mobile): the cap-anchoring contract joins the page describe, and one scroll-event helper replaces the copied event literals

* fix(mobile): a chat switch on the page opens the next chat at its newest message

The chat view stays mounted across chats, so a reader parked in one chat left
the follow owner parked for the next. On a scope change the page now resumes
following and pins to the scroller's live end before paint; the place-hold
drops what it held for the previous chat. Native keeps its current switch
behavior.

* fix(mobile): the phone page draws only the messages near the screen, in one list

On the page, RN Web's list decided which rows were in the document by index and
drew them in batches after paint: a chat opened climbing through its history
before reaching the newest message, and rows paged in above the reader were
drawn in the list's header instead, a second path where iOS stopped finger
scrolling at the seam.

The page now draws the transcript in one keyed flow: Load earlier, a top
spacer, the rows near the screen, a bottom spacer, then the live status and
waiting messages. Which rows are drawn is named by row key, so a page landing
or a cap trim never changes it. Rows drawn above the reader take their height
out of the top spacer, so nothing moves and nothing is written while the reader
touches or scrolls; a correction the spacer can't take (the place-hold's, or
making room above) is written once the reader is at rest. The follow owner
still owns the end and pins at commit. Native keeps its FlatList and props.

* refactor(mobile): the page window's coverage decision is its own pure module; the view reads the follow owner through one handle

* fix(mobile): rows arriving in a chat the page drew empty open at its end, not all at once

* fix(mobile): the page window draws from every scroll event, not RN Web's throttled report

* style(mobile): one-line comments in the page window

* refactor(mobile): the chat view reads the follow owner through one handle, keeping the file under its line cap

* fix(mobile): the page's spacer holds only measured rows, and older pages land once the reader stops

The phone page's windowed list sized its top spacer from estimated row heights. On iPhone a scroll
write stops a fling, so the estimate error could only be removed at rest: reaching the top showed
thousands of pixels of empty space, and an older page landing mid-fling grew a row above the reader
and moved what they were reading.

Now the spacer stands only for rows measured for how they draw now; rows never measured are left out
and measured in place only while following or once the reader has stopped (no touch, no scroll of
theirs for 150 ms). A page that arrives while the reader scrolls waits for that moment. Rows report
their heights through the page's one ResizeObserver, coverage is arithmetic over those heights, and
one at-rest rule puts the row the reader was reading back when anything above it changed size.
Unchanged rows keep their objects across a re-fold, so a height stays valid until its row changes.

* refactor(mobile): the page window's row registry, height scans and sync render in their own modules, under the line caps

* fix(mobile): an opened tool run, its lines and Show more stay as the reader left them when the page window unmounts the row

The phone page now draws only rows near the screen, so a row the reader scrolled past unmounts. Its
tool run's open state, each line's open state and Show more lived in the row's own state and reset on
remount, closing what the reader opened and drawing the row at a height its measurement no longer
matched. They now live in one per-chat map the page list provides (the desktop transcript's
mechanism); rows rendered elsewhere keep their own state as before.

* style(mobile): format

* fix(mobile): quality rules: explicit listener cleanup, no side effects in a state updater, kept disclosures scoped by a keyed provider

* fix(mobile): the page window moves only on measured rows, and never off the end while following

At 4x CPU the end pin's scroll event reached the coverage pass before the observer had measured the
drawn rows, so they were summed at the first-guess height, read as above the screen, and the window
shrank to the last row; the browser's clamp then looked like the reader scrolling up and a long chat
opened at its oldest message. Coverage now waits until every drawn row has a measured height (the
observer reports them before paint), and a following reader's window always reaches the end.

* style(mobile): format

* fix(mobile): the reading place is taken only from the reader's scrolls, so Load earlier at the top keeps it

A write's scroll event can arrive after an older page's rows were drawn above and before the correction; re-taking the place and covering then kept the pushed position.

* fix(mobile): only the reader's touch, wheel, pointer or key ends following on the page

Scroll to latest after a parked turn could land at the top: the window drew the end, the document shrank and regrew, and a scroll event iOS delivered after rows grew above read as the reader moving up, so following ended and the window drew where the scroll had been clamped. A move up now ends following only within a second of the reader's input on the list.

* fix(mobile): a scroll the browser makes when the page shrinks is not the reader's, so Collapse keeps the place

Collapsing tools while parked shrinks the rows above as they are re-measured; the browser clamps the scroll, and its event (delivered before the resize callback) re-took the reading place with nothing on screen and moved the window, so the reader ended at the bottom. A scroll with no input of the reader's and no scroll of theirs just before is now layout's.

* fix(mobile): the page decides once whether a scroll was the reader's, from what explains it

Three bugs today had one cause: a scroll the reader didn't make was taken as theirs (a write's late event, a clamp after the document shrank, a scroll delivered after rows changed). A scroll is now the reader's only when nothing else explains the move: not the page's write, not a commit whose resize callback hasn't run, not rows above the reading place, not a clamp to a shorter end. The window and both follow transitions use that one verdict; the reading place is re-taken after each resize callback, so a correction that couldn't land is never written later; no write lands while iOS bounces past an end. Replaces the one-second input window.

* fix(mobile): a jump away from the end while following draws where it lands, not every row to the end

The jump's scroll event covered with following still set, which keeps every row from the new start to the end: up to 1,603 rows mounted in one 1.8 s task before the follow change trimmed them.

* fix(mobile): Tools and text size keep a parked reader's place on the page

Toggling Tools regroups the rows, so the window's reading place (a row key) can vanish and nothing put the reader back: they moved about 20 messages. The place-hold, which already follows what the reader was reading through a regroup, now also runs when how rows draw changes.

* perf(mobile): measuring rows costs the same at any chat length

Each settled moment walked every measured row (resolving and comparing its state) while following, so the fill grew slower as it went (27-43 ms per round at 4x). Now one round checks rows within three screens of the reader, fills the next rows just above the measured run directly, and walks every row at most once a second; a round measures 12 rows, not 24.

* style(mobile): window hook back under its line cap

* fix(mobile): iOS bouncing past the end is the reader's scroll, not a clamp

The clamp test matched any position at or past the end, so every rubber-band frame read as layout: the page counted the reader as settled mid-bounce and an older page landed during it, with a one-to-two-frame blank. A clamp now lands exactly on the new end from a position the old one allowed.

* fix(mobile): a drawn measuring group waits for its measurement, so a landing while following doesn't redraw 30 times

After a landing while following, the next choice ran before the browser measured the group just drawn: the group sat at the top, so nothing looked owed above it and the group came down unmeasured, then went up again, 28 times in 180 ms until the pin landed. The choice now waits while the group's rows have never been measured, and re-renders only when it changes. Choosing what to measure moves to the heights; the re-check timer to the motion.

* fix(mobile): a correction puts back what layout moved, never the reader's own scroll

The browser can run a resize callback before the scroll event of a wheel, key or scrollbar move; the correction measured the reading row's move on screen, which included that scroll, and wrote it back (300 px per wheel tick while rows above were measured). The correction now puts back only the row's move within the content, keeps the scroll, and undoes only a clamp. A scroll that lands while a commit is unreconciled is left for the resize callback to tell apart, so a reader's move there still counts as theirs.

* fix(mobile): the end pin waits for the reader's move to be counted; measuring stays cheap while a reply streams

- The resize callback pinned to the end before telling apart a move no scroll event had reported, so a reader's first scroll up while following could be written back. The list now counts that move (which may end following) and then runs the pin.
- The nearby stale walk checked every measured row before its three-screen reach applied (2,236 checks per streamed update at 4x); the reach now bounds the walk.
- Measuring rounds draw 8 rows and leave 32 ms between rounds, so a streamed reply gets frames while rows are measured.

* fix(mobile): Collapse keeps the place when the browser hasn't applied a clamp yet

Collapsing shrank rows above a parked reader in rounds; when a round left the scroll past the new end before the browser clamped it, the write guard took it for an iOS end bounce and skipped the correction, and the window re-seated at the end. Corrections wait for rest, so past-the-end there is a pending clamp, not a bounce; only above-the-top stays guarded.

* fix(mobile): no second correction when the browser's own scroll anchoring already kept the row

On Chromium, rows landing above a parked reader are held on screen by the browser's scroll anchoring, which moves the scroll. The resize callback counted that move as the reader's, kept it, and put the row's content shift back on top: the row left the screen by 1.4-2.8k px. When the reading row is where it was on screen, the move is layout's and nothing is owed.

* fix(mobile): the page alone keeps a parked reader's place; the transcript is out of browser scroll anchoring

On Chromium the browser's scroll anchoring and the page's own correction both answered the same layout change, and the overlap can't be measured after the fact: a reader's wheel tick in the frame of a landing still got the landing put back twice (0.9-1.6k px). The transcript is now never a browser anchor, as on WebKit, and the end marker anchors only while following; the overlap guess (placeHeld) is gone.

* fix(mobile): no browser anchor while following either; only the page moves the scroll

The 1 px end marker stayed a browser anchor while following, so rows measured in above made the browser move the scroll to keep the end; the resize callback counted that move as the reader's and every measuring round waited for rest (spinner 36 s instead of 2.7 s on a 2,000-row open). The end pin already holds a following reader before paint and a shrink clamps onto the end, so the marker and its anchoring go.

* fix(mobile): on Chromium the page keeps a reader's place while they scroll, as the common list does

With the transcript out of browser scroll anchoring, corrections that waited for rest left Chromium readers (Android, desktop) seeing rows above change size mid-scroll, worse than main. Where a scroll write keeps momentum (every engine but WebKit), the resize callback's correction and the place-hold now write at once, as the common list does; WebKit, which stops a fling on any write and has no anchoring, still waits for rest.

* fix(mobile): a parked reader's place is the part of the row at the screen's top, not the row's own top

With browser anchoring off, growth inside the partly visible top row above the reading point (a tool card or image getting taller) left the row's top in place, so nothing corrected it: the text moved by the growth (240 px), where main's browser anchoring held it. The page now anchors the part of that row at the screen's top, as the browser's anchoring would pick; a following reader keeps the row's top, which the end pin makes enough.

* fix(mobile): on Chromium the browser keeps a parked reader's place; the page owns only following and a regroup

The page kept a parked reader's place itself with a saved element at the screen's top, which a
replaced card inside the top row took away (+240 px, main 0). Where the browser has scroll
anchoring it now holds the parked place, as main does; the end pin holds a following reader; the
place-hold takes the scroll for a regroup or a move to another row, with anchoring off from before
the update until its correction. Only input that goes the move's way makes a move the reader's
there, so the browser's moves never end or resume following. WebKit is unchanged. While following,
measuring waits for a streaming reply to pause.

* test(mobile): the page's own hold is tested as on iOS; layout settles before the reader's wheel

* style(mobile): format the page's anchoring change

* fix(mobile): the browser chooses its anchor again once the reader rests, after a scroll that landed on a spacer

A long jump can land where only a spacer is drawn; spacers are never an anchor, so Chromium chose
none and kept none after rows mounted there, until the next scroll (growth above then moved the
reader +240 px; main 0). At rest after a scroll the page nudges the scroll by a pixel and back, so
the browser chooses from the rows drawn now.

* fix(mobile): at the very top the page keeps a parked reader's place, where Chromium's anchoring never adjusts

Parked at scroll offset 0 while older rows were still being measured above, each measured group
pushed the reader down 0.9-1.2k px: Chromium makes no anchoring adjustment at offset 0, and the
page wrote no correction there. The page now takes its anchor and corrects only at the very top;
everywhere else the browser keeps the place, so the two never both move it.

* fix(mobile): after each commit the browser chooses its anchor from the rows laid out, not the container around them

Chromium keeps a partly visible container as its anchor when nothing inside qualifies; rows it
never laid out don't, and spacers never do. After a long scroll onto a spacer the anchor was the
transcript's wrapper, whose top never moves, until the next scroll: growth in the first ~150 ms
moved the reader +240 px (main 0). After each commit, while the browser holds the place, the page
lays the rows out and scrolls a pixel and back (its own writes), so Chromium chooses among them.
The settle-time re-choose goes.

* fix(mobile): the re-choose nudge is not taken as the page's own writes, so a reader's scroll landing there still counts

The nudge ends where it started, so the scroll event after it reports only what else moved the
scroll. Recorded as the page's own targets, it swallowed the End key's move to the end (within a
pixel of a target), which then didn't resume following (main and f044 did).

* fix(mobile): preserve native chat state across page windows

* test(mobile): use typed RPC fake for chat paging

* fix(mobile): on iOS a Tools toggle keeps the reader's place even while the list still glides

The page's WebKit place-hold wrote its correction only at rest, so a regroup committed within
150 ms of a reader scroll (or mid-glide after a tap outside the list) skipped it, and nothing
redid it: the text the reader was on moved by the regrouped rows' change and stayed there.
Unlike a source change, whose new rows stay out of the spacer until measured at rest, a regroup
resizes drawn rows at once. It now corrects at once, as main's hold always did.

* Preserve reading positions within phone chat scroll boxes

* Correct phone chat type declarations

* fix(mobile): a browser notice elsewhere on the phone page no longer fails a terminal that is still loading

On the page each terminal document listens for window errors, and before ready a reported error is
fatal ("Terminal failed to load"). The window is the whole app's, so the browser's ResizeObserver
loop notice, which the chat list's resize work can raise and which carries no error, reached a
terminal mounted in the background and failed it. The guest reporter now forwards only events with
an error behind them.

* fix(mobile): a parked reader stays parked when a collapse below them clamps the phone page to its end

After every resize the page attached following whenever the scroll sat within 1 px of the end,
whatever put it there. A collapse below a reader parked near the end makes WebKit clamp onto the
shorter end, so the next reply pulled them down. Following now comes only from classified motion
(the reader's own move, or a move toward the end), as on main; the resize callback only pins.

* refactor(mobile): the chat's row list, page window or native FlatList, is its own component

Main's composer and tab changes took MobileNativeChatView past its line cap once merged; the
platform choice of list and its wiring move out unchanged.

* fix(mobile): only a move toward the end makes a parked phone-page reader follow

A clamp onto a shorter end that lands while the reader's finger is still down from tapping a
collapse below them is classified as the reader's move, and any reader move ending at the end
attached following. Following now attaches only on a move down toward the end, as on main; the
test covers the clamp with and without a finger down.

* fix(mobile): a slow drag up detaches a following phone-page reader

Detach needed a single scroll event moving up more than 1px, so a slow drag that reports many 1px
moves never detached, and the next reply pulled the reader back to the end. Any upward move of the
reader's that leaves the end now detaches. Found by tracing the remaining iPhone jump.

* test(mobile): reconcile each step of the slow-drag detach test, as the browser's frame does

* test(mobile): run the slow-drag detach test as iOS Safari, where the bug was seen

* Fix following after a slow return to the chat end

* fix(mobile): closing a tool line forgets its nested command and output scroll positions

A nested box's address is the run's key, then the line's key:command, so the
old prefix match on the closed key never reached it and a reopened line
restored a stale scroll position.

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-10-19 13:50:11 -07:00
Brennan Benson 53191250e8 Accept messages while any agent starts, and bound each startup step (#28046)
* Finish structured agent readiness before delivering held work

* Publish the Codex process before protocol startup

* Publish ACP agent processes before session startup

* Publish Pi at spawn and settle signed-out starts once

* Adapt permission fixtures to startup lifecycle

* Keep model discovery for chats before their first start

* Use one host readiness promise for provider operations

* Await provider opening in the execution host

* Await Claude initialization in host-owned open

* Observe Claude readiness through the host status

* Keep optional readback behind host readiness

* Recheck launch permission after host readiness

* Preserve the awaited child during permission revalidation

* Await Codex protocol open under host ownership

* Await ACP protocol open in the host

* Derive ACP opening from its protocol session facts

* Exercise unanswered ACP options with an explicit silent route

* Await Pi open and fail signed-out construction

* Read Pi catalog facts from awaited opening

* Keep the Pi fixture exit observer

* Record failed owned opening before the first message

* Abort a matching provider open when its child exits

* Cover provider exit during an unanswered host open

* Cover explicit failure facts on a host-stopped child

* Capture message choices per accepted send and preserve picker state

* Apply accepted Claude message choices at prompt handover

* Apply captured message choices at provider dispatch

* Bind native chat choices to accepted message dispatch

* Reset Codex turn choices for each captured message

* Resolve Codex message defaults from native facts

* Align message choice and Pi default fixtures with provider dispatch

* Align option fixtures with saved message choices

* Match Codex message choices to the active turn origin

* fix: let pending commands reach handover refusal

* Abort pre-prompt message choice writes with host-owned wait

* Verify message choices through provider input boundaries

* Keep legacy mobile test hosts explicit about message choices

* Keep startup waits cancellable and preserve operation order

* Derive Codex startup exits from host phase

* Open Codex protocol in background-task test fixtures

* Keep ACP startup ownership until handshake completes

* Run ACP opening catalog fixtures with Node SQLite

* Verify saved ACP picks without live option writes

* Keep ACP catalog bookkeeping from failing an options read

* Restore signed-out restart and fail Pi startup faults

* Run Pi opening catalog fixtures with Node SQLite

* Preserve message choices across cold clients and kept cards

* Remove the unused provider option restore ledger

* Clarify launch choices and legacy option hydration

* Remove remaining restore-ledger forwarding and assertions

* Remove unused ACP and Pi restore bookkeeping

* Keep live choices and delivery order while agents start

* Drop retired startup revision from Codex opening

* Keep live-option cancellation until message choices and clarify catalog reads

* Hold ACP option replies in live-write cancellation regressions

* Remove retired Pi startup revision and match saved-pick behavior

* Check running choices and retained rewind messages after delivery integration

* Apply startup picks and let parked messages release operations [skip ci]

* Remove unused Codex opening report field [skip ci]

* Keep ACP restoration facts local to the provider [skip ci]

* Apply Pi startup picks after readiness and remove unused open facts [skip ci]

* Cover goal changes during the held Codex message wait [skip ci]

* Save a waiting startup pick after Stop ends its opening [skip ci]

* Save choices when startup ends or a replacement is still opening

* Check saved choices separately from Claude startup reports

* Release scripted Codex startup before awaiting queued picks

* Check the saved picker selection in per-message choice fixtures

* Exercise current choices and eager client capabilities in runtime fixtures

* Reuse runtime fixture reads and type the fake connection directly

* Align startup fixtures and preserve observed Claude exits

* Honor queued send authorization in Claude runtime fixtures

* Open Codex protocol sessions before Stop fixture dispatch

* Wait for the opened Codex child before runtime notifications

* Keep repeated permission picks usable and align Codex fixtures

* Validate pending picks against the live option state

* Order startup picks before later messages and retain flow regressions

* Keep reviewed lifecycle contracts while pruning duplicate tests

* Save picks after a proven exit without requiring a child

* Type the startup reply fixture against the Claude control contract

* Keep Codex startup flow contracts and remove overlapping adapter tests

* Restore the committed dependency lockfile after test tooling

* Keep startup recovery flows and remove duplicate Pi handshake cases

* Cover both Pi startup choices in the delivery flow

* Match the Pi prompt entry recorded by the scripted wire

* Honor the before-input callback in the startup reply fixture

* Keep message choice flows and remove overlapping selector tests

* Keep startup option menus independent and show the saved choice

* Exercise startup picks across real menus and the runtime

* Match startup picker fixture to saved message choices

* Keep saved effort visible when picking a model during startup

* Include the running-turn scope in repeated Stop fixture

* Use the current attach contract in import fixtures

* Keep the Codex journal translator within its line limit

* Match ACP startup fixtures to saved picks and late Stop behavior

* Check startup effort after saved picker choices are acknowledged

* Complete Stop target types and keep the effort regression concise

* Use requested picker values in the effort regression acknowledgement

* Require owned provider spawn and one host-awaited open (from #28055)

* Restore the live checks the startup cleanup's deleted tests pinned

One assertion each, in the surviving suites: a stale-generation exit is
ignored, a failed create lets its sink go, a readback before open is saved,
an idle stop that fails is retried, a no-pick Pi start teaches its default,
an older client's pre-spawn failure and packed answer, an undrivable chat
stays readable, the resting slash menu stays on its own host, the main agent
reads apart from subagents, restore publishes status, naming runs off the
host's status feed, and a throwing logger never reaches the operation.

Also corrects the reliability gate note that claimed the send-idempotency
suite replaced the refusal-retry tests.

* Read a rewind in doubt outside the chat's serialize and apply it in the commit

The started settle and the attach's ready-child path read the provider's
history first, then apply what it proves in a short commit that re-checks
the rewind it read.

* Accept messages while the agent starts: run the start outside the chat's serialize

The chat's serialize was both where a send was admitted and where its agent
started, so every slow step of a start (closing-child join, lease
reconcile, recovery probes, resolvers, history read, spawn, sink drain,
provider dispatch) held every later send.

A start now registers in one short step before its first wait: its abort
controller (which Stop, close and quit already reach outside the lock),
its startup clock (the 10-minute ceiling now counts from here) and quit's
drain. Every wait after runs outside the lock, raced with that signal, and
every write is a commit that first re-checks the start is still the
session's. A start that loses the check is late: its spawn is closed
through the post-acquisition failure path and nothing is handed to it.

- The delivery loop's prepare returns a start handle; the loop awaits the
  start outside, then re-prepares. Handover writes the pending row and
  begins the dispatch abort handle in one commit, calls the provider
  outside after re-checking the child, and resolves in a second commit.
- Ready operations claim a start instead of running one in their step.
- A client attach runs the same steps under one serialize, as before.
- An exit reported between spawn and publish is held for the publish
  commit, which ends the child it indexes.
- An unproven close still fails only what was queued when its start was
  asked for.

* Test that a send is accepted at every wait an agent start makes

One whole-flow test holds the start at each wait in turn (closing-child
join, reconcile probe, recovery re-probe, resolver, spawn, sink drain,
open, started recovery, pending row, provider call): a second send is
accepted at once, a Stop is recorded without waiting and nothing reaches
the provider, and what was sent goes to the published child once, in
order. Also quit before spawn, concurrent sends, a lease left recovering,
and an idle sweep racing a send.

* Bound each startup step on its own and drop the startup silence rule

A start no longer ends after 60 s without output: output is not protocol
progress. Each protocol step now has its own bound where its handshake
lives, and one 10-minute ceiling from registration stays as the outer bound.

- Codex: initialize 15 s and thread start/resume on the connection's 30 s
  default again, instead of the startup ceiling.
- ACP: initialize 60 s, authenticate 120 s, session new/load/resume/fork
  300 s; turns stay unbounded and option writes keep their caller's bound.
- Claude: the initialize answer has a 60 s bound; past it the open fails
  with the ordinary startup-failure words and the host ends the child.
- The onOutput plumbing that only fed the silence rule is removed.

* Count Pi ready once it reports its state

Pi's start no longer waits for its model listing and slash commands.
The open returns after get_state and the unknown-provider check; the
listing then arrives as an options report (with the catalog listing the
start resolved) and the commands as a later publish. An empty model list
no longer fails the start as signed out: a signed-out Pi runs the unknown
provider, which still fails it, and a prompt it cannot run is refused
with the sign-in words by the existing prompt path.

get_state keeps the connection's 30 s request bound instead of the
startup ceiling.

* Test that model and effort picked while a chat is created ride its first message

A slow host's create reply arrives after the user picked a model and effort. The picker's view
layers the launch's held picks over its seed and the host's saved selection, and every send
carries what the picker shows, so the first message carries the picks whether the host's status
names its selection or not, and while the last pick is still on its way to the host.

* Start a phone's new empty chat on its first message

The phone's new chat is an agent.launch with no prompt, which always created the chat with its
agent started, while desktop chats start on the first message. The phone now advertises the
create-without-start capability, and a launch with no prompt from a caller advertising it creates
the chat at rest, keyed in the ledger by its create intent as a client's create is. Launches with
a prompt, the CLI (which advertises nothing), orchestration workers and ensure still start at once;
an older host ignores the capability and stays eager.

* Take main's lockfile; the branch changes no dependencies

* Restore main's exact text in files the merge only reformatted

The pre-commit formatter rewrote 37 of main's files staged by the merge, though this branch never changed them, pushing two past the line limit. Committed without the hook because the hook would reformat main's text again; CI lints the result.

* Fit the startup tests to the owned-spawn, host-awaited-open contract

Test doubles gain the mandatory open and an acquisition generation, the
removed started event gives way to the rig's proveStart, and two literal
types are tightened for CI's typecheck. A Stop during a held start writes
no host note on this branch: the taken-back send draws its own
stopped-before-start row, which the startup-attempt test now asserts.

* Fit the remaining CI checks to the startup limits and current releases

- Run agent-launch-at-rest.test.ts in the SQLite Node runtime project.
- Pin the send-options cross-version case to v1.4.228, the first published release with agent-session.send-options.v1; v1.4.226 was never pushed.
- Load the baseline's isStructuredTab from src/shared (moved in v1.4.228), falling back to the renderer path for older baselines.
- End the never-proven start with a short startup ceiling: the silence limit it relied on is gone, and the ceiling counts from registration.

* Read the option stamp from the chat's one host-execution selection

* fix: preserve startup authority and accepted agent choices

* Retain dispatch authority and reconcile accepted options

* fix(native-chat): preserve accepted choices and cancellation authority

* Restore established client selection flow

* Preserve provider output observation and typed startup fixtures

* fix: preserve initial Claude launch choices and effort readback

* test: complete Claude catalog session fixture

* Preserve incoming main files after merge formatting

* Preserve compact handover through startup merge

* Type instant startup fixtures before their open is supplied

* Keep browser sign-in under its existing expiry owner

* Finish the main merge: retired-model display, start rewind cut, new tests

A model the Claude CLI retired is reported again, so the picker shows what the child runs and the
next send moves the chat off it. The start reads a rewind in doubt against the handle it answered
with, the record its commit then applies to. Main's tests follow the start's open: the admission
fixture holds a real turn, and a sign-in close during setup fails the open.

* Type the start's rewind record as the store returns it

* Type the Sleep test's held open as the provider open it replaces

* Sleep counts a chat stopped once the start it aborted leaves it at rest

A start runs outside the chat's lane, so Sleep's step can run before the start it aborted has
settled; it then re-reads the chat once that start is done, instead of reporting it unverified.

* Keep Sleep's start-abort bookkeeping beside the rest of Sleep's stop
2026-10-19 10:53:46 -07:00
Jinwoo HongandE2E Test 20fc7af557 test(relay): drill a memory cell through a database outage, and pin phone renewal (#30666)
* feat(relay): add the per-cell phonePath switch, effective only in reserve mode

Step 6 of the relay redesign keys every phone-path change on this switch. The cell
reports the effective value in runtime status, and the flag tool refuses phonePath=memory
unless the cell is reserve, and a reserve flip that would turn phones over unnamed.

* feat(relay): resume paired phones from one device read on memory cells

With phonePath=memory in reserve mode, a phone resume is admitted from one device
read and the cell's own seat, and takes no activity leases. A refused device closes
4401, a failed read 4503, and only a missing device row falls through to the invite
path. A phone whose host has no seat gets one assignment read: 4409 when the row
names another cell or the read fails, otherwise 4404.

* feat(relay): keep a memory resume's basis in the cell and confirm it from one read

A phone resumed on the memory path keeps its connection basis in the cell, not in
relay_connection_bases. Its confirm rereads the device row once, renews it only when
less than 29 days remain, and always answers a current-version confirm renewed:true
with the expiry the database holds. Confirms route by where their basis lives, so a
flip between accept and confirm completes either way. Retries answer from a 10-min
in-memory result map.

* test(relay): roll a memory phone cell back with confirms in flight

Flipping back by admitMode=db, phonePath=db or a dead-man trip leaves in-flight
confirms to finish on their memory basis, writes no phone lease and leaks none, and
evacuation still refuses the cell while Postgres records it as reserve.

* feat(relay): batch a memory cell's resume-confirmed audit rows

Memory confirms buffer their audit rows and the cell writes them as one multi-row
insert every 5 s or 500 rows, idempotent by a per-event id. The buffer holds at most
5,000 rows and drops a row older than 10 minutes rather than retrying it, counting
and logging both kinds of drop.

* feat(relay): count and log phone-path failures, and every attach deadline

The runtime metrics line gains phone refusals by close code, attach deadlines, accept
database errors, resume confirm failures by code, completed accepts by phone path and
the resume audit drop total. Every attach-deadline 4404 now logs what the cell knew of
the desktop's control (pong age, ping outstanding, control age and round trip), so a
half-open control can be told from a slow data dial.

* feat(relay): close an attach-deadline 4404 with the attach-timeout reason

The phone close reason from #30607 now rides the close frame of the 4404 a cell
sends when the desktop's data leg misses the attach deadline, so OTA phones can
redial the same cell once instead of waiting out the host-offline delay. The failed
relay-hello frame is unchanged, and old phones ignore the reason.

* test(relay): drill a memory cell through a database outage, and pin phone renewal

A blocked database leaves splices forwarding, closes new resumes 4503 and fails
confirms cleanly, lets an in-flight confirm finish on its memory basis after a flip
back, and drops audit rows past 10 minutes instead of writing them late. On the phone,
a relay-only session lives 31 days under the once-a-day renewal rule, and would not
if a not-due confirm answered renewed:false.

* fix(relay-ops): store phonePath=db as no key, so old images keep taking writes

An explicit phonePath=db left in the switch file would make the flag tool refuse every
later write after an image rollback, including a watcher's admitMode=db flip back.

* fix(relay): log a failed no-seat read, recheck the switch after the device read

A failed assignment read for a phone with no seat now logs a rate-limited line before
its 4409. A switch that turned off during the device read sends the phone down today's
path. resolveResume reuses the one-read lookup, so the expiry and grace rules have one copy.

* fix(relay): log a failed memory renewal; test flips, replays and basis cleanup

A renewal write that fails still answers the stored expiry, but now logs a rate-limited
line so a persistently failing update is visible before phones age toward re-pairing.

* test(relay): assert a rolled-back memory splice issues no statement when it closes

* fix(relay): flush resume audit early only on crossing the batch size, drop the shutdown flush

A failing database no longer takes a 500-row insert per confirm; retries wait for the
5 s timer. The shutdown flush raced the pool closing, so it is gone and the at most 5 s
loss is documented. A stopped buffer no longer re-arms its timer.

* test(relay): assert the aged audit rows were dropped, not written

---------

Co-authored-by: E2E Test <e2e@test.local>
2026-10-19 13:24:18 -04:00
Brennan Benson 04548087a5 fix(mobile): edit a queued message in a plain text box with compact cancel and save buttons (#30382)
* fix(mobile): edit a queued message in a plain text field with compact cancel and save buttons

* fix(mobile): the queued edit field grows from one line to four in the phone's web page

* fix(mobile): the queued edit's cancel and save buttons take 44pt-tall taps, and the queue no longer jumps while typing

- Measure the edit field's height in a hidden copy of the textarea instead
  of collapsing the live one: WebKit clamped the queue list's scroll
  position on every keystroke. It also re-measures when the field's width
  changes (rotation).
- react-native-web drops hitSlop, so the X and check were 28pt targets.
  Each button is now a 44pt-tall target reaching its neighbours, with the
  28pt square drawn inside; the layout looks the same.
- Announce saving with aria-busy, which react-native-web maps.
- List the new web sibling in web-overrides.json.
2026-10-19 10:17:21 -07:00
Jinwoo Hong cf13e0cf9d fix(sidebar): phone feed list leftovers before the flip (dark) (#30631)
* fix(sidebar): phone feed list leftovers before the flip (dark)

Bump the feed's semantics version for P11's desktop facts; mark switch-on phone page bundles so packaging and orcad staging refuse them; show the page's last-listed rows before the first feed read; draw the host's pending creations; pass the listed host's own label into host sections instead of the running device's platform.

* fix(sidebar): review fixes for the phone feed leftovers (dark)

A dark orcad may stage the dark page (only a marker of an off switch is refused); the bundle tree writer leaves the marker; the pre-feed list keeps the view's filters; a creation names its project only outside that project's header and survives search when its project matches; the feed view and row labels read one resolved desktop platform.

* fix(sidebar): second review fixes for the phone feed leftovers (dark)

Test the off-switch marker check across switches; resolve the desktop platform once for rows and headers; a section search narrows keeps only matching rows.
2026-10-19 04:27:09 -04:00
Neil f184c13bd7 Align root Node types with the supported Node 24 runtime (#30617)
* Align root Node types with the supported Node 24 runtime

* Preserve mobile rejection listeners with Node 24 types
2026-10-19 00:46:25 -07:00
Jinwoo HongandClaude 9ea5434371 fix(mobile): faster relay recovery after an app switch and on a late desktop data link (#30607)
* fix(mobile): recover a relay that died during an app switch in one short probe, and retry an attach timeout through the director once

Refs STA-11130.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(mobile): redial the same cell once on attach-timeout instead of re-resolving

Aligns with the relay step 6 design: reason string attach-timeout, no director
re-resolve (the director names the cell the host is seated on).

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(mobile): probe decisively only on a real background return, 4s, and jitter the attach-timeout redial

Co-Authored-By: Claude <noreply@anthropic.com>

* test(mobile): replace type assertions with a typed fake in the relay probe tests

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 03:30:06 -04:00