The shell ends a host stream after end/error even when the screen stays
mounted; the page now drops the entry so a re-subscribe cannot deliver
twice.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Codex round 2, shell lane: navigation and workspace-creation results that
land after client replacement or disposal are rejected instead of
registering stale handles; a direct session-tab subscription cancelled
before its snapshot waits for host registration and unsubscribes by its
own id so it cannot leak or cancel a sibling.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Per-field limits did not guarantee the aggregate review fit the shell's
byte envelope; the projection now admits files and the newest comments by
remaining bytes and flags truncation.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The desktop wrapper already types and redacts the search result; the page
strips unknown fields with the contract schema like readDir does, and the
last shell-era sanitizer module goes.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Codex review of the native store: Swift and Kotlin both accept the mermaid
frame as a third document, remove superseded generations once their last
session closes, and the session hook retries once per build id so a
desktop upgrade gets its own recovery.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Codex review of the shell lane: the workspace authority keeps only the
worktree handle map; the single-entry subscription wrapper, duplicate
byte checks and arbitrary 128-stream caps go; ended streams are removed
before reconnect replay; a retired snapshot pager cannot restore a stale
workspace handle after client replacement.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Codex review of the desktop lane: browser input, markdown tab, project table
and review query call the runtime directly instead of looking up RPC methods
by name; the workspace stream subscribes to client events and tears down on
abort; the browser input token bucket and command dispatch shim go; remote
transcripts identify by path so appends are not read as replacements; page
subscription acks with a body reject ready; git write requests carry the
60s deadline.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The packaging test and fixture read the embedded document names from the
shared manifest contract instead of mobile/src, which the root CI job cannot
load (no Expo toolchain). Merge two duplicate-module imports the focused
plugin audit rejects, and format the files oxfmt --check flags.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The shell forwards page traffic blindly; the few desktop methods it still
calls are its own duties (worktree handles, terminal tab resolution, the
multiplex, device speech, the cancellable commit-message run). Pin them so
a translator cannot creep back and re-pin the APK.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The hosted page's task operations live under mobile/src and reach the
desktop through the mobileWeb host set, which the gate admits alongside
the native allowlist.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(relay): never cache a region hint from a one-region catalog
The director lists only regions with a serving cell, so a roll wave shortens the
catalog to one entry. The resolver required only every *listed* region to be
measured, so that lone region won against nothing and was cached for 24 h: a US
desktop refreshing while US cells rolled published asia-east2 for a day, the
incident #19233 was written to end. Now fewer listed regions than the fleet serves
withholds the hint (1 h no-hint TTL), the same outcome as an unmeasurable peer.
* test(relay): give the unstable-probe case a two-region catalog so it has one cause
* Revert "feat(mobile): time relay dial stages so diagnostics say where a slow connect went (#19245)"
This reverts commit 83b1558ecc.
* Revert "perf(mobile): race the direct and relay dials from t=0 on every reconnect (#19308)"
This reverts commit ceafdcad2f.
* Revert "feat(mobile): draw the last known tab strip while a session reconnects (mobile pass) (#19281)"
This reverts commit 643571def6.
* Revert "perf(mobile): open a session with parallel startup RPCs and a pre-warmed terminal engine (#19260)"
This reverts commit c37413271e.
* Revert "perf(mobile): cut the relay reconnect critical path and admit dead sockets faster (mobile pass) (#19280)"
This reverts commit e628090ad4.
* chore: keep the react-doctor suppression for the startup timers
The pattern it covers (a variable number of timers cleared through one cleanup)
predates #19260 and is unchanged by the revert; dropping the entry only re-exposed
a pre-existing finding to the changed-code gate.
Restore the workspace hostSubscribe arm the browser and workspace merges
dropped between them, retire the settings capability the workspace lane
moved onto ui.get/ui.set, replace the task lane's page RPC sender with the
shared one, and re-pin the census, grant and correlation tests to the
merged registry.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(native-chat): scope composer file drops to the pane that received them
A native OS file drop resolving to `target: 'composer'` carried no pane
identity, so the window-wide payload was attached by every mounted composer.
Because inactive chat tabs stay mounted (hidden), one drop populated every
chat pane's attachment cache, and those chips replayed whenever the user
returned to a tab they never dropped into. The workspace-creation composer
and chat composers also leaked into each other, since neither could tell
which surface actually received the drop.
Composer drops now carry a `scopeKey` the way a terminal drop carries its
tab and pane leaf id: the composer publishes its pane key as
`data-composer-scope-key`, the preload harvests it during the composedPath
walk, and each composer attaches only its own. The workspace composer's
last-wins ownership stack now claims unscoped payloads only.
* test(native-chat): supersede the bug-asserting drop repro with the scoping test
The repro that landed on main asserts the pre-fix behavior (a drop reaching
every mounted composer), so it fails once drops are scoped to the pane that
received them. Its scoping cases now live in
native-chat-composer-drop-scope.test.tsx, which keeps its editor-target
control case verbatim and adds coverage for unscoped composers and a scope
key published inside the drop-target marker.
* test(native-chat): cover workspace composer drop isolation
* fix(native-chat): authorize external attachment paths before preview
---------
Co-authored-by: Merge Sim <sim@local>
* fix(native-chat): stop an unanswered host from reading as one that refuses structured chat
`readLocalRuntimeCapabilities()` returned `[]` both before the first status probe
landed and after one failed, so "not asked yet" and "host says no" were the same
value. Every structured-chat launch route consumed it, and an unprobed host was
routed to legacy chat exactly as a refusing one is.
Keep the two apart: the cache holds `null` until a probe succeeds, a failed probe
leaves it `null` rather than emptying it, and the launch route names the case with
its own blocker instead of borrowing `runtime-capability`.
No routing outcome changes — both cases still decline structured chat. The point is
that the reason is now truthful, which is what the routing work needs to build on:
once a launch can target a runtime peer, capabilities come from that host, and an
unanswered remote must not be indistinguishable from one that refuses.
`hostCapabilities` on the launch route stays local-only at every call site; a
per-target resolver replaces it when the route learns to reach a peer.
* test: cover unknown runtime capability lifecycle and launch fallback
---------
Co-authored-by: Merge Sim <sim@local>
A terminal artifact is now addressed by the tab plus the terminal text that
named it. The desktop re-resolves the path and re-earns the file grant on every
chunk, so a retired terminal cannot keep serving bytes and there is no token,
TTL or record cap to keep. `artifactRelease` has nothing left to release.
An agent session is addressed by the agent and provider session id the scan
already reports, and paging is a caller-supplied offset. The vault's own row id
embeds the transcript path, so the projection publishes the provider id instead
of the composite. The resume mutation key is derived from the session, and the
host's existing create-result TTL owns its lifetime.
The agent-history host calls now go through the typed runtime functions the
sibling RPC handlers call, with no dispatch by method name and no re-parsing.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The shell CSP pinned the markdown editor's inline script and the mermaid
frame's inline script, so editing either one needed a new native binary.
Both scripts now ship as content-addressed package assets that their
documents load by src, and the editor moves from a `data:` frame that
inherited the shell policy to a package document of its own.
`'self'` cannot carry these: the frames are sandboxed, and WebKit resolves
`'self'` against the frame's opaque origin, so it matches nothing there
(Chromium resolves it against the response URL and does match). The native
frame policy names the per-session package origin instead.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The rich markdown editor document interleaved its markup and its one inline
script across five modules, so the script could not be served on its own.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Clearing page readiness in an effect let one paint show the outgoing page's
readiness against the incoming document. Keying the state on the session and
view epoch resets it in the same render that changes them.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The page now drives the same desktop requests the native app makes, through
an RpcClient-shaped sender over workspace.hostRequest, so the shell's read and
source translators and their contracts are gone. Creation itself stays in the
shell: a workspace no catalog page has listed has no page handle, and only the
authority can mint one. Repo ids cross as host ids, so the re-lookups that
existed to re-resolve opaque repo handles are deleted with them.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The shell capped every forwarded call at its 15s default, which is shorter
than an SSH connect needs. The envelope now carries an optional deadline the
caller sets through the same request options it already passes, clamped to
180s so a page cannot park a host call indefinitely.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The download now buffers each asset in memory, verifies it, and hands the store
one complete file, so the chunk protocol and its per-chunk hashes go with the
stage ids. The session/recovery/refresh/capability hook chain collapses into one
reducer-driven hook with no prop-drilled refs, and A/B rollback goes with it:
there is no earlier generation to promote, so a page that cannot load makes the
shell delete that host's cache and download again, once per host selection.
That retires the health deadline, the process-failure tracker, the cached-build
probe, rejected build ids, and the whole recovery toolbar.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The device harnesses polled `activation.json` to learn which build was live.
A host now keeps exactly one committed generation, so that directory is the
record. The crash-loop and corrupt-cache drills only ever asserted the A/B
rollback and are removed with it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
A strict zod schema on a parsed value already rejects `__proto__`, unknown keys,
and unsafe numbers, so the 280-line hand-written scanner only ever bought two
things: a repeated key, which `JSON.parse` silently resolves before the schema
sees it, and an unpaired surrogate, which parses into a perfectly valid string.
The test states both, and the replacement checks only those.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The desktop was parsing the page's payload contract under a placeholder
workspace handle, because the contract named a workspace the desktop never
learns. Each payload is now built once per scope from one field shape: the page
keeps `workspaceId`, the desktop gets `worktree`. The wrappers declare the real
schema instead of a passthrough, and results carry no workspace handle at all.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The wrappers called the RPC dispatcher by method name and got `unknown` back, so
the projection rebuilt every field with typeof checks and regex before a final
parse. That is the shell's sanitizer moved rather than retired: the desktop is
where these values come from.
Each wrapper now calls the typed runtime command directly, so a review is
`HostedReviewInfo` plus a discriminated `GitHubWorkItemDetails` or
`GitLabWorkItemDetails`, and the projection is a field map with a provider split.
The clipping stays, because comment, file, check-job and diff-row counts are real
size caps the provider does not honour.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb