accounts.list, the three selection methods and accounts.subscribe now reach
the desktop through the generic lane, and the page parses the snapshot with
its own schema. The shell keeps only the reset-credit arms, which mint a
native idempotency key and so cannot be a plain forward.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
worktree.activate/set/sleep/rm, repo.list and ui.get/ui.set now reach the
desktop through workspace.hostRequest, and a mobileWeb.workspace.subscribe
wrapper reduces the client-event firehose to bare catalog change types. The
shell keeps only the worktree.ps catalog read that mints workspace handles;
repo handles are gone from that path, so the page addresses repos by host id.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Deleting the grant took the mode check with it, so `workspace.hostRequest`
would forward a subscribe method. The desktop answers that with `streaming:
true` frames, and `RpcStreamRegistry.handleResponse` claims every one of them
before the pending-request map sees it, so the unary promise hangs to its 15s
deadline while the desktop subscription stays open with nothing to cancel it.
On the relay transport the pending map wins instead and the promise settles on
a partial first frame, which is wrong in a different way.
The naming rule already answers this without a grant: a method has a derivable
cancel name exactly when it is a subscribe method. `prepareMobileWebHostRequest`
now returns that derivation, `hostRequest` refuses a method that has one, and
`hostSubscribe` refuses a method that does not. Both raise
`unsupported_capability`, the code the grant-mode mismatch used.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The RPC deletion removed a check the native app really made. On the hosted page
readability was constant true, but the native shell resolved the workspace's
repo and hid grok and omp tabs whose transcripts the runtime cannot read, which
is every repo behind a non-runtime-owned SSH connection. Deleting it changed
released native behavior.
Restore the gate on the native path only: `isMobileNativeChatTranscriptReadable`
and the `nativeChatRequiresLocalTranscript` branch in `resolveMobileNativeChat`,
the readability hook, and the flag through the controller, active resolution and
terminal action sheet. The hosted page implements the same operation as a local
`true`, so it stays ungated and still makes no bridge call.
Readability is sourced from `repo.list` as before. The only native repo store is
a 60-second host-scoped cache written by the host screen and the new-workspace
dialog, which a deep-linked session route cannot rely on.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Three suites still named `workspace.hostCatalog`: a page mock branch, a
saturation slot that the grant-index filter already skipped, and a ceiling
assertion. None of them reached a registered operation any more.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The page reached a desktop method by first reading `mobileWeb.host.catalog` for
a grant, then forwarding through it. Every grant field duplicated something the
desktop already enforced: the allowlist is the mobile-scope socket gate, the
byte budgets are the bridge envelope, `workspaceParam` was `worktree` in every
entry, and `scope` was "did the page send a workspaceId".
The shell now forwards `workspace.hostRequest` and `workspace.hostSubscribe`
straight through. It rewrites the opaque workspace handle when the payload
carries one, checks one envelope in both directions, and derives a stream's
desktop cancel name from its subscribe name: `X.watch` to `X.unwatch` and
`X.subscribe` to `X.unsubscribe`. Unary versus stream is decided by which shell
operation the page called. `files.unwatch` and `nativeChat.unsubscribe` keep
their names for the released native app and gain `mobileWeb.`-prefixed aliases
sharing the same handler.
Shell feature negotiation goes with it: `init.shellFeatures` had no consumer
beyond its own tests, so the protocol version is the bridge's only compat gate.
That constant and the package bridge range move into `bridge-limits.ts`, and
`bridge-protocol-version.ts`, `bridge-release-policy.ts` and
`shell-feature-contract.ts` are deleted.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The hosted page owned two shell destinations. `terminalSettings` was already
dead: the page's own device operations push `/terminal-settings`, which
`host-web-app/` serves. `connectionLog` was the last live one, and the hosted
`/connection-log` route renders the same `ConnectionDiagnosticsScreen` over the
same bridge-backed native diagnostics operations, so the host screen's
diagnostics link now navigates in place instead of tearing down the session
view to reach the shell copy.
With no destinations left, `MobileWebNativeRouteHandoff` and everything that
carried a request id to it go: the route enum members, the handoff record and
replay, the broker-message wrapper that existed only to complete it, and the
page-document and navigation-authority plumbing. `handleMobileWebBrokerMessage`
collapsed to a single broker `handle` call, so the shell calls that directly.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
`mobileWeb.nativeChat.readability` answered `true` for every workspace: the
host treats native-chat eligibility as its own and each transcript read
validates its execution provider independently. The page still paid a bridge
round trip per workspace to learn nothing.
Delete the RPC, its page-catalog grant, both page clients, the operation
contract, and the shell hook that consumed it. With the flag gone the
`nativeChatRequiresLocalTranscript` pre-gate in `resolveMobileNativeChat` is
dead, so grok and omp tabs now resolve like every other supported agent and a
transcript the host cannot read surfaces as a read error rather than a hidden
chat.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The Relay integration harness and the remaining shell fixtures still
faked bind and page-session round trips, so they proved a protocol that
no longer exists. They now assert the host ids the page actually sends.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* Revert "feat(mobile): draw the last known tab strip while a session reconnects (#19258)"
This reverts commit 0ba7f8dc8d.
* Revert "perf(mobile): cut the relay reconnect critical path and admit dead sockets faster (#19236)"
This reverts commit 23df74d85a.
The shell mirrored the desktop's opaque handles with a browser authority,
a native-chat binding cache and a page-session lifetime subscription, and
the page bundle bound a resource before every read, write and file
action. With host ids in the session snapshot none of that maps anything:
the page's tab id is the host tab id and its session id is the provider
session id.
Chat and terminal actions now travel in a single host call, the shell
resolves a chat session against the live tab list when it needs a native
terminal handle, and the image ledger keeps its own bound rather than
riding on the deleted binding cache.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Unpairing cleared metadata, overlays, and credentials but left the
host-scoped page preference blob behind forever, since nothing else is
keyed by the pairing key. Remove it when no remaining host still holds
that key, best-effort so it cannot fail an authoritative removal.
The host list mutation queue moves to its own module to keep host-store
under its line budget; it is the same chain, only named.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The native route inlined 245 lines of orchestration the hosted screen had
already factored behind DiagnosticsDeviceOperations, so the two paths
gathered, redacted, and submitted the same report differently. The screen
now takes the device operations, a clipboard writer, and a host picker
slot; the native route supplies the existing native operations and its
host chips.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The hosted adapter rejected a configure whose echoed receipt differed from
the request while the native adapter accepted any receipt. The paired
desktop is trusted and ships the page bundle, so the extra check only
produced a failure the native path does not have.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The native and hosted settings routes each hardcoded the same seven rows
and had already diverged: Troubleshooting carried a different icon in each
and the availability flags only existed on one side. One builder now owns
the rows and takes the hosted shell/page-preference availability, so the
routes keep only what is genuinely theirs — credential cleanup on native,
external links on hosted.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The load path set the error but left busy pinned, and every control is
disabled while busy, so a single failed preference read left the screen
inert with no way to retry. Clear busy in a finally as the voice settings
screen does.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The broker parsed the stored blob before dispatching on the action, so a
corrupt or oversized blob failed every request forever — including the
clear that would have repaired it. A reset now runs before the size check
and the parse, and a read serves empty values with a single warning rather
than a hard unavailable. Writes still refuse to overwrite data they cannot
read.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Every getItem crossed the shell bridge as its own one-key multiGet, so the
five preference reads a settings screen issues on mount exhausted the
shell's four-concurrent request grant. Adjacent same-action calls in a tick
now fold into one request bounded by the contract's 64-key limit, and
flushGetRequests dispatches the pending batch instead of doing nothing.
That removes the reason product code hand-serialized its reads, so the
sequential awaits in the terminal preference loaders and the ad-hoc
in-flight accessory dedupe go with it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): draw the last known tab strip while a session reconnects
Reopening a workspace the phone has already visited threw away everything
it knew. The route clears its tabs on mount, so until the reconnect lands
and the first snapshot is applied the session screen has an empty header
and a bare spinner, even though the strip it is about to be handed is the
one it drew a minute ago.
Persist the four fields the strip actually draws -- id, type, title, agent
-- per host and workspace, and add a reconnecting-with-cache shape to the
route state so those rows render immediately, disabled, under the ids the
live snapshot will reuse. Live tabs always outrank the cache, so a
mid-session drop keeps its mounted terminals; an exhausted retry loop or a
rejected pairing outranks it the other way, because a strip the user cannot
reach is worse than the existing offline affordance. With nothing cached
the screen behaves exactly as before.
The body stays a placeholder. Replaying stored scrollback into the terminal
WebView would double-render the same rows once the live stream replays them,
so the strip is the cached content and the body waits for the stream.
* fix(mobile): keep shell titles and unpaired hosts out of the cached tab strip
Review of the reconnect strip cache found two ways it leaked.
A terminal's title is whatever the shell last set, which is routinely the
command line: a psql URL with an inline password, a curl with a bearer
token. Both fit well inside the 64-character cap and both were written to
plaintext AsyncStorage verbatim. Browser tabs carried their page title the
same way. Terminals and browsers now collapse to a fixed label, with a
resolved agent naming itself because that lookup is a closed enum. The rule
lives in the storage module rather than its caller, so it holds for entries
an older build already wrote, and a tab type this build cannot draw is
dropped instead of having its title trusted.
The cache also survived forgetting a host. Nothing expired an entry, and
the module-global memory map meant a later save from any surviving host
serialized the forgotten host's rows straight back to disk. Both cleanup
paths now evict by host, dropping the in-memory rows and rewriting storage,
with a pending debounced write cancelled so it cannot restore them.
Also: the storage key digests the workspace id, which ended in a filesystem
path, and cached rows carry the same de-emphasis as the disabled tab-bar
buttons beside them, so an inert row does not pass for a live one.
* perf(mobile): cut the relay reconnect critical path and admit dead sockets faster
Phone medians put E2EE authentication at ~424ms but `connected` at ~630ms,
because the session serialized two RPC round trips behind it: the resume
confirm (`pairing.getEndpoints`) and the capability advisory. Both now ride
the authenticated socket concurrently and off the critical path, so the
session publishes `connected` as soon as E2EE authenticates. Peer identity
is already proven by then — the confirm carries credential/lease bookkeeping
and the cell assignment check, and it still fails the session on a bad answer
or a foreign relayHostId, only later. `persistResumeConfirmation` awaits the
new `whenResumeConfirmed()` instead of assuming the answer is present at
`connected`.
Foreground liveness on a retained relay: `notifyForeground('app-resume')`
now probes past the 10s voluntary minimum on urgent bounds (2s, one miss),
so a socket that died while the process was suspended is admitted in ~2s
instead of ~8s. Focus and network nudges keep the old minimum and bounds.
Relay sessions also gain a 25s idle sweep, gated on foreground so a
backgrounded app spends no probes.
Recovery is no longer blocked by the direct return probe. The probe's 12s
dial is a pure observation on its own socket, so it takes the supervisor's
operation mutex only for the cutover; a relay recovery landing during a
foreground return now starts immediately instead of waiting the budget out.
Requests that do land during the cutover are queued in a new
RelayRecoveryIntentQueue and replayed on release — an owning forced
replacement keeps its intent, everything else replays as a plain recovery.
Tests updated deliberately, for the new ordering:
- 'sends no periodic traffic while an authenticated relay is idle' asserted
the absence of any relay idle probe, which is exactly the gap D3 closes.
Replaced by a sweep test plus a backgrounded no-probe test.
- 'rate-limits foreground sequences without suppressing a retry' asserted
that app-resume was suppressed inside the 10s minimum. An app resume is
now the one nudge that must never be rate-limited.
- the session helpers waited for the confirm answer before `connected`;
they now authenticate, read both concurrent frames, and settle them.
* fix(mobile): book backoff when a relay resume confirm fails after the cutover
Review round 1 on 352bfd2300.
P1: publishing `connected` at E2EE authentication made `migrateTo` resolve
before the resume confirm answered, so a confirm that failed afterwards —
a `relayHostId` mismatch from a rehomed desktop is the live case — was still
reported as an `established` dial. registerFailure was skipped, no cooldown
was booked, recordMigration()/setActiveSession() ran for a dying session, and
the queued-recovery replay redialled immediately: a tight loop with a
connected→disconnected blip per pass. The establisher now awaits
whenResumeConfirmed() after the cutover and, if the session is no longer
connected, reports a failed dial (or an aborted one when direct won or the
supervisor went inactive) exactly as a rejected migrateTo used to. The UI
still connects early; only the supervisor's bookkeeping waits.
The state check, rather than getFailure(), is the oracle: a live session can
carry a latched failure without having failed yet, and "is this session still
alive once the confirm settled" is precisely the question migrateTo used to
answer.
P2: the resume probe profile goes to two 2s misses instead of one. The first
frame after a resume rides a cold radio and a possibly distant cell, so one
slow answer is not proof of a dead link; the verdict still lands at 4s rather
than the previous 8s.
Nits: the direct probe's two early returns no longer close the candidate the
finally also closes (the second shape pre-existed); RelayRecoveryIntentQueue
is cleared in the supervisor's stop().
Mutex-hold note: persistResumeConfirmation, and now the establisher's own
await, are bounded by the confirm's request timeout. That would have been the
session's 30s default, so the confirm is pinned to RELAY_CONFIRM_TIMEOUT_MS
(12s) — the same bound migrateTo's waitForAuthenticated applied before.
Test: a supervisor-level case where every dial authenticates then fails the
confirm must book 250/500/1000ms backoff with no immediate redial, and must
never record a migration. It fails on the pre-fix establisher.
The desktop's page-method catalog is a module constant, so its grants can only
change when the desktop process restarts, which tears down the socket and makes
the broker replace the client. Reading it over the wire before every forwarded
request cost a full round-trip per host call and, at mount, several identical
reads at once. Cache grants per client with in-flight dedupe, keyed by method so
a sibling's cancellation cannot fail a peer, and clear it wherever the broker
already clears per-connection authority state.
The host-request concurrency ceiling lived twice: a literal 4 in the accounting
module and a grant limit the shell never read. Take it from the grant alone and
raise it to what a product page needs, counting only one-shot forwards, since
subscriptions have their own ledger cap and catalog reads no longer reach the
wire per request.
Byte ceilings disagreed in both directions. The grant schema let a desktop
advertise a size a shipped shell hard-rejects, so cap it at the shared bridge
envelope, and give the read-heavy desktop methods that whole envelope instead of
an arbitrary 512 KiB. Forwarding also serialized each payload up to four times
per direction; one walk now yields both the verdict and the length.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Each `it.each([[true, true]])` ran a single case while advertising a matrix, and
the branches guarded by `host && shell` were dead. Naming the case says what the
test proves, and deleting the constant branches leaves only the lane that runs.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Under --adversarial-content the journey had no native baseline, so it derived
the expected changed-file label from the page it was checking and then asserted
the page contained it. That assertion could never fail. The expected label now
comes from the adversarial repository fixture that created the changed file, and
the journey refuses to run when neither an independent path nor a native
baseline is available.
The workspace-row tap that follows the journey moved next to it, so the step
owns the row it returns to and the runner stays under the .mjs line ceiling.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The page resource ledger hid host terminal, browser page and provider
session ids behind opaque handles. The Desktop is trusted and the page
ships inside its installer, so the indirection defended nothing while it
wedged headless workspaces after 128 epoch admissions, rejected
interleaved reads, and cost two extra round trips plus a full tab
enumeration per user action.
Ids in the projected snapshot are now the host's own, chat and terminal
methods take {tabId, sessionId} and resolve once per call, and the
session feed removes its inner listener by the key that feed registered
rather than one guessed from the caller's selector.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Session snapshot/feed/activate/close/createBrowser/createTerminal,
quick commands and agent options now run as Desktop mobileWeb.* adapters
behind the generic host-request forwarder instead of shell translators.
Voice, notification, terminal, browser, chat, About and diagnostics
settings render as hosted page routes with native-*/web-* operation
backends. Adds the hosted WebView e2e journeys used to validate them.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Require the first-release hybrid baseline and remove 23 superseded shell operations, fallback adapters, and optional settings paths while preserving native RPC compatibility. Keep large Files, Source Control, and native-chat responses bounded in Desktop adapters, and serialize Terminal preference reads within bridge admission limits. Validate all code gates, rebuild and export, and pass serial iOS and Android adversarial journeys; record remaining domain work and certification gaps.
* fix(native-chat): remember structured chat model and effort picks
Structured Claude and Codex sessions already read the saved launch
options at create, but nothing ever wrote them back. The only writer of
`nativeChatSessionOptions` was the PTY picker, and the composer swaps in
the structured surface for structured panes, so a structured pick went
nowhere: it was forgotten when the session ended and every new session
started at the CLI default.
Persist a settled pick from both the desktop and mobile structured
surfaces. Model and effort are stored as a pair, because a launch
resolves a stored effort only under a stored model — so an effort-only
pick adopts the model it was chosen against, otherwise the remembered
effort never reaches a launch at all.
Two things the persist path deliberately avoids: it writes what the
provider committed rather than what was requested, since Codex
reconciles an effort the newly selected model cannot run; and it never
writes the provider readback, which is the CLI's own default and would
pin a `-m` the user never chose.
* fix(native-chat): persist session option picks atomically
---------
Co-authored-by: Merge Sim <sim@local>
* feat(cloud): add the mobile push gateway and its contract package (#8129)
A small open-source service that holds the APNs key and FCM credentials and
sends background push to paired phones on the desktop's behalf. Hosts
authenticate with a box challenge and HMAC proof on their pairing key, the
same shape the relay uses, so signed-in and accountless desktops share one
path. Tokens are stored; alert text is held only for the coalescing window.
The contract doc in docs/reference is the source of truth for every wire
shape. The interop test runs the real desktop answerer against a real
gateway-issued challenge so transcript drift fails in CI.
* feat(push): register phones and send background push from the desktop (#8129)
Adds the notifications.remote-push.v1 capability, the registerPush and
unregisterPush RPCs on the mobile allowlist, a gateway client with a cached
session and 401 re-auth, a durable unregister outbox, and a dispatcher that
offers every mobile notification to the gateway after the socket fan-out.
The dispatcher is fire-and-forget with one retry and drops registrations the
gateway reports dead.
Puts agentState on the mobile frame and fixes the #4375 wording so a working
agent is never announced as finished. The relay host-proof code moves onto a
shared envelope module with no behaviour change.
* feat(mobile): background push registration, receive, and settings (#8129)
Fetches the native APNs or FCM token, registers it with every paired host
that advertises the capability, and re-registers on token change. Foreground
pushes are suppressed inside handleNotification against the same seen set
the socket path uses, so nothing shows twice. Taps route by host fingerprint.
One Background notifications switch, off by default, with the disclaimer and
needs-input / finished sub-switches; hidden until a paired desktop is new
enough. Adds google-services.json and the expo-notifications plugin.
* chore(cloud): Terraform and deploy workflow for the push gateway (#8129)
Declares the Cloud Run service, runtime account, secrets, and orca_push
database behind push_gateway_enabled, true only in production. The deploy
workflow is gated like the relay's, deploys with no traffic, probes /ready
and a validate-only FCM send, then shifts traffic. It runs as the shared
production deploy account because the Cloud SQL rollout lease grant is
foundation-owned; its extra authority is three bindings on the push service.
docs/push-gateway.md carries the import commands for the resources created
by hand and the APNs key rotation procedure.
* docs: describe background notifications on the phone (#8129)
* docs: check in the mobile push contract (#8129)
Seven committed files cite it as the source of truth for every wire shape;
docs/reference is allowlisted per file, so add the entry.
* test(push): replay one checked-in host-proof vector on both sides (#8129)
Cloud Verify installs only the cloud workspace, so the gateway suite cannot
import the desktop answerer. Replace the cross-workspace import with a fixed
challenge vector generated from the contract package; the gateway fixture and
the desktop answerer each replay it and must produce the same HMAC. A
transcript drift on either side now fails in that side's own suite.
* fix(cloud): open the push gateway with invoker_iam_disabled, not an allUsers binding (#8129)
The production domain-restricted-sharing policy rejects an allUsers
run.invoker member, which the runbook anticipated. Opt the service out of
invoker IAM the way the relay director already does; the host proof is the
authentication either way.
* docs(cloud): the push.onorca.dev record exists and is hand-managed (#8129)
* fix(push): close review findings in the gateway (#8129)
- Quota reservation takes a per-host advisory lock; READ COMMITTED admitted
a whole burst past the cap (80/80 without, 60/80 with, against Postgres 16).
- Challenge issuance no longer writes push_hosts; the row lands on proof
verification. Stale hosts prune after 30 days. Per-IP token bucket on the
two unauthenticated routes.
- Streaming body limit via hono bodyLimit; a chunked body bypassed the
Content-Length check.
- registrationIds deduped in the schema; per-host device cap of 64; list
bounded to its schema.
- Gateway-side challenge TTL is the specified 10 s, not 40 s.
- APNs stream settles on close as well as end/error.
* fix(push): close review findings in the desktop client (#8129)
- A gateway registration the registry cannot persist is enqueued for delete
instead of leaking a live token.
- Unregister outbox re-reads pending per pass, honours enqueues during a
drain, and retries with backoff instead of waiting for the next launch.
- Dispatcher batches registrations by 20 rather than starving the rest.
- 401 compare-and-clear; a 401 after re-auth is unreachable; refused
handshakes and 429s are cached briefly instead of re-handshaking per event.
- Service is stopped on quit.
* fix(mobile): close review findings in push registration and receive (#8129)
- Consent generation guards a register that finishes after the switch went
off; the host is re-queued for unregister instead of recorded live.
- Foreground pushes seed the watermark before adopting the epoch, so a push
on a never-connected session cannot wipe a valid watermark.
- aps-environment follows the build via app.config.js; the iOS release
workflow sets it to production. A bare plugin entry wrote development.
- Pushes the OS showed while closed are marked seen before catch-up replay.
- Token null result is not cached; failed capability probes are retried and
never block an unregister; coalesced summaries are shown but not marked.
- Unresolvable fingerprint routes nowhere and is suppressed in foreground.
- Android channel ensured at boot; capability hook diffs clients by identity.
* fix(cloud): harden the push deploy workflow and size the gateway to the budget (#8129)
- Roll traffic back on a failed post-shift check; delete a candidate that
never took traffic; retry the origin probe and the FCM probe.
- Assert Terraform-owned scaling instead of mutating it from the workflow.
- Build before taking the Cloud SQL rollout lease.
- Declare the database pool in Terraform (2 per instance, max 2 instances)
and add the gateway to the connection budget; the previous default put the
shared instance 65 connections over its ceiling.
- State plainly that the shared deploy identity's relay authority is inherited.
* fix(push): read the runtime from shared state at push startup (#8129)
Threading the runtime through launchDesktopMode put the launch module one
line over the 300-line lint budget after the rebase.
* fix(push): key the unauthenticated rate limit on the hop Cloud Run wrote (#8129)
Cloud Run appends the connecting peer to x-forwarded-for; the limiter read
the left-most value, which the caller controls, so a forged first hop earned
a fresh bucket per request.
* fix(push): close the final security review findings in the gateway and infra (#8129)
- app.onError logs only the error name and answers a bare 500; hono's default
handler printed the whole error, and a pg error carries the row in detail
- a second per-IP bucket (240/min) runs ahead of the bearer lookup on every
authenticated route, so forged bearers cannot spend the two-connection pool
- one live session per host: minting deletes the host's earlier row
- device-less hosts are pruned after 1 h, not 30 d; any keypair mints one free
- notificationId is printable ASCII, since it becomes the APNs collapse header
- the impersonated FCM probe token is masked in the workflow log
- prevent_destroy on the Apple secrets and the orca_push database
* fix(push): close the final security review findings in the desktop client (#8129)
- fetch never follows a redirect: a 307 would replay the host proof and the
phone's token to whatever origin the redirect named
- registerPush params are strict and the paired identity is spread last
- a per-device bucket (10/min) bounds a phone looping registerPush, which
costs a gateway write and a synchronous registry write each time
* fix(mobile): close the final security review findings in push receive (#8129)
- a push with no epoch can no longer claim a seq-derived dedup key, in the
foreground or from the tray; a forged seq:N could otherwise swallow the
real bell at that seq
- a provider-delivered push with no host catalog, or no fingerprint at all,
stays unrouted instead of falling back to the hostId its raw data carries
* docs(push): record the ip buckets, session and host retention, and the token-ownership limit (#8129)
* fix(push): apply the schema on an untimed pool and retry statement-timeout aborts (#8129)
Ports the relay's #18722 pattern to the gateway: DDL runs on a one-connection
pool with statement_timeout 0 that is closed before the serving pool opens, and
SQLSTATE 57014 joins the bounded transaction retry path.
* fix: harden mobile push delivery and deployment recovery
* feat: align mobile notification preferences with desktop delivery
* fix: accept variable-length APNs device tokens
* fix: deduplicate native APNs and background socket notifications
Record the verified Terminal consumer journey alongside Chat and Browser results. The optional OTA crash-loop fixture is deferred outside the worktree under the requested YAGNI scope.
Exercise text scale, autocorrect and custom shortcut persistence through the real hosted settings and session. Reuse picker and terminal inspection logic and restore original preferences. Full iOS adversarial run passed with ok:true against build eab7fd8f5eb4a37e593526e90dcc5105552691db072da8a982cd1ed30784a2e3; all code gates and export passed.
Move presentation to shared screens and persist terminal scale, autocomplete, shortcut layout and custom keys through paired-host page preferences. Reuse native fallback and external links, and correct native restore-setting response unwrapping. Deliberately update session parity and route census assertions. Full code gates and page export pass; iOS consumer verification is running.
Add negotiated host scope, Desktop-owned session terminal creation and native-chat file actions. Reuse execution-host routing, existing mutation identity and bounded forwarding; preserve legacy fallback before dispatch only. All required code gates and page export pass in the integrated tree; iOS interaction validation is in progress.
Share native presentation and apply paired-host page preferences to terminal link behavior. Preserve inherited native values, handle storage failures, and verify saved values plus Chat route recovery through a real iOS WebView restart.
Negotiate bounded page-owned state while retaining legacy routes. Preserve settings through WebView recovery and cold-resume handle rebinding; explicit native navigation clears page state. Cover the actual init envelope and old-page fallback.
Bind terminal identity before opening its stream and revalidate before clear, rename and display-mode dispatch. Reuse authenticated runtime handlers and preserve pre-dispatch legacy fallback without retrying ambiguous mutations.
Reuse the Chat UI settings screen with paired-host page storage and grant-gated navigation. Verify persistence and theme rendering in the full iOS adversarial journey; retain native recovery presentation.
Resolve host-owned chat resources and reuse terminal.send for authenticated TUI actions. Feature-gate the page consumer on dispatch fencing, share the action deadline across binding and execution, and preserve unknown delivery without fallback after mutation dispatch. Keep native image and persistence capabilities and old shell/host paths. Record green gates and corrected iOS evidence.
Bound catalog and request lifetime and check page/workspace authority after connection waits. Fence retired physical requests across logical cutover, preserve existing response ambiguity, and advertise dispatch support without changing protocol 2. Record the discovered catalog authorization gap in the tracker.
Add bounded native preference storage and route hosted AsyncStorage through it. Preserve namespace and credential isolation, serialize writes, and reject unavailable or corrupt storage. Remove the inert adapter and reconcile the implementation tracker with completed generic streams and platform evidence.
Retain future transcript fields, validate host-owned bindings for every event, and use per-stream cleanup tokens. Preserve legacy host/shell fallback and cancellation semantics. Required gates and page export pass.
Scope resource handles to the connection, workspace and shell-injected page session. Desktop validates current chat bindings and retains future transcript fields; hosted reads use the generic lane with legacy fallback. All required gates and page export pass.
Wait for the diff body rather than just Review controls, and handle Back through Source Control before opening file previews. iOS confirms terminal links and rendered adversarial diff; the full unattended journey remains tracked. Required gates pass in native-chat-read-gates.