* fix(chat): preserve Claude rewind context after rollback
Fixes STA-11420.
* fix(lint): merge duplicate journal-types import
* fix(chat): skip torn Claude transcript lines when materializing a rewind fork
* fix(editor): name a server's file by its owned id from the first open, so it no longer reloads after a restart
* refactor(mirror): a server file's mirror id no longer needs the bare-path collision lookup
* fix(editor): conflict review reuses an open server file under its owned id instead of opening a second copy
* fix(editor): conflict review overview shows an open server file's unsaved edits under its owned id
* fix(editor): route conflict files by their workspace owner only; keep the overview lookup a plain read
* test(mirror): the browser-placement replay test names its server file by the owned id
* feat(ci): ratchet raw repo connectionId host reads; one drop-in resolver
Codex review 6 class fix (#4/#6/#7): host decisions read off a repo row's
legacy connectionId miss rows that name SSH only as executionHostId.
- getRepoRoutingConnectionId (shared/execution-host.ts): drop-in for a raw
repo.connectionId routing check that also finds executionHostId-only SSH.
- check-repo-host-read-ratchet: per-file count of raw repo connectionId
reads in src/main, src/shared, src/cli; counts may only shrink.
- Migrated 66 cheap reads: runtime GitHub/GitLab commands, PR/MR head ref
fetches, preserved-branch cleanup, ref queries, base-ref IPC, worktree
preparation, trash sweep, scan cache TTL, usage metadata.
* fix(ipc): PR base IPC routes executionHostId-only SSH rows; ratchet is a stated lower bound
Review follow-up (Opus on #31096):
- worktrees:resolvePrBase decided git/gh routing on the raw connectionId while
its head-ref fetches already used the resolver; one resolved connectionId
now drives all of it (test fails on main).
- getRepoRoutingConnectionId's contract is stated exactly: never local when the
raw field is set; on a contradictory ssh row executionHostId wins.
- The ratchet regex no longer matches report.connectionId; header and docs
say it is a lower bound.
- Regression tests for a runtime GitHub and GitLab command site.
* test(ci): pin the repo host-read ratchet step to the static phase
* fix(ci): repo host-read ratchet skips verbatim field copies into projected rows
A copy of connectionId into a projected row decides nothing; migrating it to
the resolver would change what the projection publishes. The sidebar feed
catalog's copy (added after the baseline) and two older projections no longer
count; baseline pruned to 91.
* fix(native-chat): rewind works at any chat length
Rewind copied the kept part of the chat into its saved record and, for
Codex, re-read and re-translated the whole provider history before and
after the revert. Both were bounded (10,000 rows / 2 MB host, 1,024
entries / 2 MB Codex), so long chats were refused with "too long to go
back in".
The record now names the cut; the kept rows are derived from the
journal, which keeps the old epoch until the rewind completes. Codex
reads only the turns it undoes and trusts thread/revert; recovery
proves an applied revert from the newest turn alone and never
truncates otherwise.
* feat(native-chat): Claude chats launch a rewind's fork of the kept conversation
A rewind record can name the provider session the chat continues in (providerFork). Claude's next
launch forks it with the SDK's own truncating resume at the rewound prompt's parent, and publishes a
forked link once the provider has created it. Removes the transcript proof branch that could never
pass on the real CLI.
* test(native-chat): pin Claude's rewind fork against the real CLI
* feat(native-chat): rewind Claude chats
Claude declares rewind. The adapter answers where a rewind forks (the rewound prompt's parent in
its own transcript); the host saves that fork with the prepared record, stops the agent, and
rebuilds the journal from the rows before the message. Recovery completes a prepared fork, since
nothing outside Orca can have half-happened, and the next launch makes the fork.
* test(native-chat): keep the Claude rewind tests to the flows they pin
* test(codex): type the rewind fixture's page params instead of asserting them
* ci: run the PR checks against main
* fix(native-chat): rewind recovery reads the provider handle only from a record that exists
* fix(native-chat): import the session record type once
* test: run the rewind cut test in the SQLite runtime project
* test(native-chat): run the fork rewind test on SQLite, and give Claude's test double its fork
* fix(native-chat): omit rejected submissions from rewound history
* fix(native-chat): rewind forked chats without restarting the agent
* fix(native-chat): Claude rewinds also leave out messages that never ran
* fix(native-chat): Claude rewind to a mid-turn message drops the rest of that turn
A message sent while Claude is answering joins the running turn. Rewinding to it
forks Claude's conversation at that message, so the chat now drops the rest of
that turn too instead of keeping replies Claude no longer has.
A rewound chat whose kept conversation lives only in another Claude account now
gets the same "history is in another account" refusal as a resume, instead of
Claude's raw start failure on every launch.
* fix(native-chat): rewind only to messages that start a turn
A message sent while the agent is still answering joins that turn. Claude's CLI
holds it until a break and records it after the reply; Orca's chat records it
when it was sent. Rewinding to it therefore cut the chat and Claude's
conversation at different places: the previous fix dropped reply text Claude
still remembered, and the version before kept the message itself. For Codex the
cut started at the turn's own first message, removing a message the user did
not pick.
Rewind now returns only to messages that opened their own turn, the common
pattern: the chat stops offering it on a message sent mid-reply, and the host
refuses one, so a mid-reply message and its turn are always kept or dropped
together. The Claude cut goes back to keeping a kept turn's late rows.
* fix(native-chat): rewind targets follow the turn record's opener; saved rewinds still complete
A message opened its turn when the turn record names it, not only when it sits
at thread level: restored history scopes its openers to their turns, so those
chats lost Rewind.
The opener check now runs only when a new rewind is admitted. Recovery cuts
wherever an already saved rewind points, so one saved by an earlier build
still completes instead of blocking the chat.
* fix(native-chat): recovery's read-ahead reads turns only from a Codex rewind cut
* wip(layout): applyDefaultTabs command in the layout model (dark)
* wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one
* wip(layout): main closes or keeps an exited split pane by the window's rule (dark)
* wip(layout): a create or split sent again after its close is refused (dark)
* wip: typed test doubles
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e)
* wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark)
* wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark)
* wip: lint fixes (max-lines, test throws)
* test(layout): a terminal close stands after a real failed profile write; switch off still refuses
* fix(layout): checked types in the close-failure rule and its tests
* fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark)
* test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed
* fix(layout): the switch-off create path gains no await
* fix(layout): a failed start never writes back a pane the user closed (dark)
* fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark)
* test(layout): D1a's switch-on close tests assert the intent main receives (both builds)
* test(layout): the close fixture answers main's command without naming the method
* fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark)
* fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark)
* fix(layout): a pane's start status ends when its pane binds again by any path (dark)
* test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design
* fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on
An exited pane closes through the legacy retirement unless the keep rule holds; the
default tab's title and color still reach the window and the phone's live tabs.
* fix(terminal): one id guard for every pane write; keep panes on an unvouched -1
Main's taken and closed-id guard now runs where every pane write goes, so a runtime create
replayed after its tab closed is refused before it spawns. An exit code nothing vouches for
keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the
switch-on tests that asserted the deleted window hand-off and title bridge.
* fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands
A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab
id is refused only for a client's own layout command.
* fix(terminal): a provider-reported -1 keeps the pane, as the window does
Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that
the closed-tab guard never fires in the shipped build.
* chore(terminal): satisfy the exhaustive-switch and test typing checks
* test(terminal): give the replay test's live process a cwd
---------
Co-authored-by: E2E Test <e2e@test.local>
* fix(ssh): an old SSH host converts while a relay shell named only on its tab still runs
The conversion check counted legacy pane-key aliases as terminals waiting to
reconnect. An alias only routes hook events to a pane whose terminal the lease
check, the bridge or the terminal gate already accounts for, so it no longer
blocks the move.
* Tighten the alias census comment
* Register the tab-only relay spec: typed deployment lookup, relay fence baseline, PR route
---------
Co-authored-by: E2E Test <e2e@test.local>
* The window shows each layout records frame record by record, without reading its session back (dark, layout-owner build only)
A records frame no longer re-serializes the window's whole session and runs the
Loader. Panes' terminal bindings and live titles apply alone, to the tabs the
frame names; any other record builds only its own workspace's slices, with the
view and facts read from the store. Only a workspace the store does not show
from frames yet (startup, a reopened stream, a first visit, one sent whole)
takes the full build.
Tests install an apply check that, after every flush, compares the store with
the full build for the same cache and pre-flush store; the shared layout-apply
transport now sends main's records frames and fails a flush that differs.
* A frame of only tab rows shows without reading the window's session back or running the Loader (test, layout-owner build only)
* The account-restart test's fake binding runtime carries per-pane binding generations, so it passes with the switch on (test only)
* The account-restart test types its store writes, so its five old e2e type errors are gone (test only)
* A workspace build reads the editor selection and groups through the save's own writers, as the full build does (dark, layout-owner build only)
* A tab released from the window's hold shows the binding main wrote meanwhile (test, layout-owner build only)
* The frame-apply check still checks a flush's other workspaces when it builds one in full, and the shared transport keeps a test's own check (test only)
* The frame-apply check is proven to catch a mismatch beside a full build, and the shared transport restores only the check it installed (test only)
* Name the pane tree the window compares by what it holds: the tree without its panes' bindings or live titles (dark, layout-owner build only)
---------
Co-authored-by: E2E Test <e2e@test.local>
Main runs one pass at startup and each window load, with or without a window
(desktop, orca serve, orcad): every live local terminal no pane names is bound
to the pane its record names while main still holds it free, else shown in a new
tab bound to it. The window no longer asks main to adopt terminals; it only reads
that main holds them. The window's layout.apply loses its adopt field.
Main also derives activeWorktreeIdsOnShutdown, remoteSessionIdsByTabId and
activeConnectionIdsAtShutdown from its own layout, terminal records and SSH
connections, replacing the window's copies in every save and the quit checkpoint.
Co-authored-by: E2E Test <e2e@test.local>
Behind ORCA_LAYOUT_OWNER_IS_MAIN, a CLI or phone file open is applied by main as an
openEditorTab layout command, so a host with no window can open files. The window
reports each workspace's active group as an optional view fact on syncWindowGraph,
and shows main's tab once the frame that draws it arrives. Editor language
detection moves to shared so main can name a file's language.
Co-authored-by: E2E Test <e2e@test.local>
* fix(renderer): a twin repo id refuses instead of following the Active Server focus, and a listing for a repo with no row is this app's
findRepoOwner no longer lets focus pick between same-id repo rows: runtimeTargetForRepoOwner refuses a
twin, as resolveOwner does, and getRuntimeEnvironmentIdForRepo names no server for one. repoHostId
stamps and routes a repo with no row as this app (or a web client's only server), never the focused
server.
* fix(renderer): a fork names its source's host, and GitHub routing keeps its twin rule until it can refuse
* fix(renderer): a fork captures its source's server transport, and a GitHub edit on a twin refuses instead of using this computer
* fix(automations): a new-per-run workspace is created on the run's captured host
* fix(automations): pass only a captured run host, so a twin without one refuses
* test: stamp the server repo row through a store helper, prune the focus-read baseline
build-orcad-template.mjs takes --dark-test-template to let ORCA_LAYOUT_OWNER=main
through for test servers; the template and out/orcad carry the switch marker, and
packaging refuses a marked template. The e2e template steps build a switch-on server
when the workflow runs with layout_owner=main.
Co-authored-by: E2E Test <e2e@test.local>
* test: shared switch-on fixtures so 71 layout-owner test files run with the switch on
- layoutApplyTestTransport answers layout.apply (accept, apply to main's model with its frame, pending, refuse)
- main's loaded layout as snapshot frames, shared from the frame-sync test
- tab-layout maps for the editor store harnesses; missing store methods in the main harnesses
- workspaceRepos/registerWorkspaceRepos so main's load keeps seeded layouts
- ORCA_LAYOUT_OWNER=main runs vitest with the switch on
- the 4 rollback-build tests stay switch-off, labelled
* test: keep switch-off assertions exact, settle only after layout commands, reset the transport per test
* test: parse the transport's command without Reflect.get; name the staged-session type
* test: pin the test transport in the layout.apply census; route the autosave fixture as unit support
---------
Co-authored-by: E2E Test <e2e@test.local>
* fix(renderer): no route resolver reads the Active Server focus, so a local row never routes to the focused server
The worktree route's single-focused-runtime branch, the folder owner's focus fallback and the
worktree host default's focus fallback are gone. The client stamps every server row on arrival, so
an unstamped row is this machine's: terminal spawn and teardown, browser tabs, quick-open, chat and
path links now agree on that. resolveOwnerWithLegacyFallback folds into resolveOwner, and the
focus-free write route becomes the one operation route. Setup terminals keep their explicit
focused-runtime choice.
* fix(renderer): setup terminals keep their focused-runtime host, unstamped repos stay local for the composer, and fixtures stamp server rows
* chore: merge duplicate route imports, drop new fixture casts, prune focus-read baseline
* fix(orcad): ship node-pty's winpty backend so Windows 1809 hosts can open terminals
node-pty selects winpty below Windows build 18309, but the Windows server
slot carried only the ConPTY addon, so the candidate's PTY self-test failed
on Windows 10 1809 / Server 2019 and the host was recorded as unsupported.
Reported by @lksr202 in #30906.
* test(orcad): smoke the winpty backend in the Windows prebuild slot
* fix(ssh): say the host dropped this desktop's pairing, not that another Orca holds the port
* fix(ssh): pair again over SSH when our own managed server refuses this desktop's token
* fix(ssh): key re-pairs by the refused revision, cap attempts, retry on explicit connect
* fix(ssh): keep a newer pairing stored mid-repair, count only completed reads, and offer Connect, not an access link, for managed servers
* fix(ssh): judge a re-pair only against the pairing still stored; Check connection retries a managed server's pairing
* fix(ssh): a pane whose managed server refuses this desktop says why and offers Reconnect, instead of spinning
* test(e2e): live failure path for a refused managed pairing
* fix(ssh): a Connect that re-paired answers from the new pairing, not the retired one
* fix(ssh): only an explicit Connect lifts a managed re-pair's hold and cap; say SSH access can pair again
Startup restore reaches ssh:connect with automatic: true and went through the same
ensureTunnel that cleared the hold, so repeated automatic reconnects could bypass the
three-attempt cap. The reset now lives in connectTarget's explicit branch.
The Shared Server Access footnote now says revoking there doesn't stop someone with
SSH access to this computer from pairing again.
* ci(e2e): route the managed pairing-repair spec with the managed-host remote sources
* fix(ssh): the repair gate decides whether a managed tunnel builds, not the build
A refused shared-control open rebuilt the tunnel (SSH execs, forward, handshake, wake)
before the held failure or the attempt cap was consulted, so reopens on a blocked
server rebuilt without limit. buildWithPairingRepair now asks the gate first: a failure
stands for the refused pairing revision for 15 s, or through the window once the cap is
spent, and every caller gets it without reaching the host.
- A connect during an in-flight repair keeps its reset (per-environment reset epoch).
- An unpaired answer fails at once; it is not a moved port, so no re-read or wake.
- A refusal that only rebuilt the tunnel no longer reports "pairing changed".
- WSL and cloud VMs no longer say Orca tried to pair over SSH, and Check connection
only re-pairs servers managed over SSH (runtimePairingRepairRoute).
- Translate the new strings; route the pane-banner files to the managed-SSH e2e lane.
* fix(ssh): fence a status re-pair follow-up to the same server, and hold a refused repair under its own revision
- getRuntimeEnvironmentStatus followed any pairing change mid-probe, past a caller's
expectedEnvironmentPairingRevision fence. It now follows only for an unpinned caller and only
a new pairing of the same SSH-managed server under its pinned host key.
- recordRefusedAfterRepair stood under whatever revision was stored now, so a newer pairing
stored mid-refusal was blocked untried; it stands under the repaired revision.
- The WSL restart test expects the refused-pairing classification main's WSL path now reaches.
* fix(ssh): a re-pair hands its callers the new credentials, and a failed one judges only the pairing it tried
- ensureOrcadManagedTunnel returns the environment as stored once the tunnel stands. The
terminal census and a deploy's already-current result use it, so after a re-pair they
no longer authenticate with the refused token, which read as an unverifiable census and
refused restarts, updates and stops.
- The shared-control refusal handler blocks the status owner only while the failure stands
for the pairing stored now (managedOrcadPairingFailureStands). A newer pairing stored
meanwhile keeps its owner and retries.
* fix(ssh): migrations commit with the pairing the tunnel stands on, and a Connect mid-handshake stays reset
- Conversion binds the environment ensureOrcadManagedTunnel returns, so the destination, the
server project reads and the result use the re-paired credentials (the migration mark is kept).
- A delta move gets its destination only from connectDestination, which opens the tunnel and
builds the destination from what it returns; a pre-ensure destination can no longer exist. A
tunnel that cannot open keeps the delta journaled and the host marked for the next move.
- buildWithPairingRepair takes the reset epoch when it starts, so a refusal after the re-pair
cannot reinstate a failure that a Connect pressed during the handshake cleared.
* refactor(ssh): one module owns a shared control socket's environment policy, keeping request-connections under its line limit
* feat(orcad-migration): unsaved drafts stay on this desktop and are renamed to the server on commit
* fix(orcad-migration): refuse clashing drafts from the rename's own collection; stricter live spec
* fix(orcad-migration): stamp renamed drafts with the server, rename at startup after a cut-off commit, keep draft refusals
* fix(orcad-migration): retain only after a startup rename is durable; startup survives an unreadable journal
* fix(orcad-migration): a cut-off commit is renamed and retained at startup, so the rename runs once
* fix(orcad-migration): one retain per migration at a time; no rollback that could drop newer edits
* test(e2e): check the visible draft in whichever editor a large file opens in
* fix(orcad-migration): startup retains only migrations of hosts this profile owns
* ci(e2e): route the editor-drafts migration spec to the sources it covers
* fix(provider-process): kill a provider group its SIGKILLed supervisor leaves behind
* fix(provider-process): re-read a provider start in its read second; poll instead of sleeping in tests
* fix(provider-process): keep the report channel through the Codex session adapter; re-prove same-second starts
* fix(provider-process): never kill a leaderless provider group; list guard files in the CLI project
* fix(layout): restored terminals named only on their tab reattach after a restart (dark, layout-owner build only)
In a layout-owner build, a terminal recorded only on its tab (an older session, or an SSH tab
whose relay session was never visited) started a new shell after a restart and orphaned the old
one, and an unvisited SSH tab lost its relay session id on the next save.
- Main's Loader binds a one-pane tab's own terminal to its unbound pane in a workspace that was
live at shutdown, as the window's reconnect does, so main's frames and saves hold it.
- The window's reconnect writes the restored row terminal again: ptyId is a field the frame merge
leaves to the window, so main's frames never set it on a restored row.
* fix(layout): main binds a restored tab's own terminal once at load, not on every Loader run (dark, layout-owner build only)
The Loader re-derived the binding on each load from fields the window rewrites live, so an exit
main recorded could be undone. A switch-gated Store load repair now binds a one-pane tab's own
terminal to its unbound pane, as the window's reconnect does, and main's saves keep it. It skips
a terminal another mounted pane or a tab-bar tab holds, and a paired server's partition.
* fix(layout): an SSH workspace's restored tab binds only its relay session id; repair runs after the load purge (dark, layout-owner build only)
* test(layout): the reconnect gate test expects the row ptyId write; cover the repair's purge order and SSH repo predicate
---------
Co-authored-by: E2E Test <e2e@test.local>
* fix(runtime): a failed orphan adoption write rolls back instead of keeping the adopted tabs
The adoption rollback ran outside the store's durable transaction, so the store
rebased it like a stale renderer write below the topology fence and kept the
adopted tabs, layouts and incarnations; the next save then persisted them.
Run the adoption through runDurableMutation, whose rollback phase skips that
rebase, and refuse if a queued write changed the session it was checked against.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(runtime): an adoption into an unwritten host partition is not refused as a conflict
Co-authored-by: Claude <noreply@anthropic.com>
* test: run the orphan adoption rollback test in the SQLite runtime project
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(runtime): check and write an orphan adoption inside the store transaction
A snapshot comparison could not see a queued binding write that edits the
session in place. Plan the adoption against the session the transaction
reads instead, so queued writes are validated against rather than guessed at.
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
#29150 removed one layout write from tabs-group-actions.ts without pruning
its baseline entry, so every PR's static analysis fails with 'baseline still
allows 13'.
* fix(diff-comments): a review note on a twin worktree saves to the open copy and names its host
* fix(diff-comments): rendered notes and queued saves follow the open twin
* fix(diff-comments): one host-qualified copy per change; async delivery finds its copy by note id
* fix(diff-comments): a copy is its row's own host; unstamped rows keep the owner their repo names
* fix(diff-comments): the open host picks a twin by the owner index and never falls back to another host's copy
* fix(diff-comments): a worktree's saves stay in one queue when its row gains a host stamp
* fix(diff-comments): one queue and floor per worktree id; paired servers stay separate copies
* refactor(diff-comments): keep main's queue and floors; scope rows only when two hosts publish the id
* fix(diff-comments): per-twin rollback floors that follow the mutation chain; write module split out
* fix(diff-comments): a queued save finds its copy by the list its chain produced
* fix(diff-comments): name the row's own host on every save, and treat a refused save as failed
* fix(diff-comments): a queued save keeps its rows' identity even before a twin appears
* fix(diff-comments): a queued save follows its row when a listing stamps it with the write's host
* fix(diff-comments): refuse a write when the open host's copy isn't listed and another host's is
* fix(diff-comments): a lone unstamped row's queued save survives a stamping refresh
* fix(renderer): host writes route only through the strict owner, and twin projections stay on their host (review-5 #2#9#11)
A selected ssh: host that two paired servers proxy now resolves ambiguous at the one
selected-host resolver, so no caller can fall back to this app's own SSH connection.
File Explorer and SSH mutation expectations derive the full {endpoint, place} pair from
resolveOwner, so a server folder's nested SSH target stays the write's host. A local
lineage write projects onto its own host's rows only. The id-only store-write ratchet
now also covers persistence repo-row lookups by id alone (review-5 #10's shape).
* fix(ratchet): the id-only lookup scan also sees optional chaining and destructured ids
* fix(renderer): an unstamped folder keeps reading its host from its repos; editor focus fallback left for a follow-up
* docs(renderer): the File Explorer owner comment names its folder fallback
* ci: fail the static job on unformatted files
Runs oxfmt --check over the same tree pnpm format writes (~2s), and makes the
pre-commit hook format markdown and YAML so it covers what CI now checks.
* ci: route docs-only and workflow-only diffs through the formatting check
The format check reads the whole tree, so a diff that skipped the static job
could land drift that fails every later PR. Any changed file oxfmt formats now
selects static analysis (not typecheck or units), like the mobile-only route.
The path detector checks out .oxfmtrc.json for its ignores. The hook no longer
fails a commit whose only matches oxfmt ignores (lockfiles).
* perf: preserve matching Terminal settings sections during search
* fix(settings): keep a retained Terminal sessions section fresh and share the section stack
Move the search-stable section wrapper into SettingsSectionStack so every settings
pane keys sections by their own element key instead of the array index, and subscribe
ManageSessionsSection to the daemon session inventory invalidation signal so a kill
routed through another surface still reaches a section that no longer remounts.
Restates the read-count oracles as node identity plus non-growing read counts so a
future freshness fix does not read as a regression.
* test(settings): mock the Windows administrator section in the Terminal section-lifetime suite
* style: format Terminal section-lifetime reliability gate
* fix(native-chat): a chat's visuals folder lives as long as the chat
A chat's scratch folder was deleted once the chat had gone 30 days
unused, so an old chat's inline visuals turned into "Visualization
unavailable" while the chat and its history stayed. The common pattern
keeps a chat's files for as long as the chat and never deletes them on
an "unused for N days" timer.
The sweep now removes only folders no chat record maps to, plus
leftover .expired folders older builds parked. Everything that existed
only to make the age rule safe goes with it: the per-folder save lock
and its expiry candidates, the session-lane idle check, the journal
last-activity query, and the migration's fresh-window timestamp. The
visuals skill no longer tells agents the folder is temporary.
* fix(native-chat): an image sent in a local chat lives as long as the chat
A local chat's pasted images lived in one paste folder that deleted
every file older than 30 days, sent or not, so an old chat's images
turned into label chips. Paired-server chats already keep pastes in the
host's attachment store, where the send that names a file claims it for
its chat and only unclaimed files are ever swept: the common pattern.
A local composer paste now goes into this machine's own attachment
store (starting the local chat host first), so its send claims it the
same way. The old paste folder is no longer written or swept: messages
sent before this change may name its files. Restored drafts still read
it.
An unclaimed upload now waits 30 days instead of one before the sweep
takes it. A draft names its image by this path, so the window is how
long an unsent draft keeps its image; 30 days is what local drafts had,
and it outlasts the host's send-id window so a resent draft still finds
its image.
A chat folder an older build set aside as unused (<hash>.expired) goes
back to its chat instead of being deleted.
* fix(native-chat): a terminal-backed chat's pastes are never swept
A terminal-backed chat has no journal, so no send claims its pastes and
nothing can tell a sent one from an abandoned one. Its pastes therefore
stay in the paste folder, which is no longer swept, rather than in the
attachment store, whose sweep would take them after 30 days unclaimed.
Only a local structured chat's paste (forStructuredChat) goes to the
store, where its send claims it.
* fix(native-chat): keep the attachment owner's literal kind for structured local chats
* test(ipc): expect the local chat store option on the clipboard registrar
* fix(native-chat): preserve attachment ownership and restore held files safely
* fix(native-chat): settle restored image checks independently
* test(native-chat): assert restored image outcomes without batching
* Re-check draft images when the attachment sweep removes their upload
A draft checks its pasted images once, when its composer comes back. Every
chat in an open workspace mounts its composer at launch, so that check ran
before the store sweep (60 s after host start, then every 6 h) deleted an
expired unsent upload, and nothing checked again: the chip lost its picture
with no Not kept feedback and Send stayed enabled.
The sweep now reports each removal attempt; the main process forwards it to
its window, and the draft store starts a fresh finite check for every loaded
local draft image in that folder, mounted or not. A notice is a reason to
check, never proof of absence.
* Let the app effect remove the draft re-check listener it adds
startNativeChatDraftLoad dropped the unsubscribe for the upload-removal
listener, so each recovery through the root error boundary's Retry remounted
App and added another listener. Return it so the effect's cleanup removes it.
The localization audit treated the CSS inside three feature-wall <style>
elements as rendered copy, so the extractor wrapped 7-10k character style
sheets in translate() and every locale carried them. A translation could
then rewrite the CSS, and no pack could translate them under the
per-string cap. The audit now skips <style> and <script> children; the
three sheets are plain strings again, and their keys leave all catalogs
and the runtime-required English catalog.
* fix(renderer): list a pane main holds before the window mounts it, so a tab close by a split's handle finds it (dark, layout-owner build only)
Co-Authored-By: Claude <noreply@anthropic.com>
* test(e2e): the split-close spec polls through the host's "can't answer yet" terminal list
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(runtime): a pane the window lists before main binds its terminal keeps main's terminal (dark, layout-owner build only)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(runtime): a tab close by a main-side split's handle stands on main's own record of the pane (dark, layout-owner build only)
Replaces the window-side listing and the binding fill: the window's list lags main in three
ways (built before main wrote the pane, from it before the binding, or before the window mounts
it), and only main's own layout answers all three.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(runtime): main's relisting for a close tells no clients (dark, layout-owner build only)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: E2E Test <e2e@test.local>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(terminal): a pane main first sees at its binding goes where the window split it (dark, layout-owner build only)
* fix(terminal): keep main's divider positions when taking the window's split tree
* test(terminal): main's divider above the window's new split is kept
* ci: the layout-owner switch-on job runs the hidden-terminal load spec that caught the split graft
* test(terminal): type the side-by-side fixture as a split so ratio spreads typecheck
---------
Co-authored-by: E2E Test <e2e@test.local>
* fix(persistence): worktree meta and lineage writes take the row's host, not the id (R4-2b)
* fix(persistence): review fixes for host-targeted worktree writes
* test: SAFETY notes on partial runtime store mocks
* test: drop imports duplicated by the rebase onto #30797