* fix(files): never write through a dangling symlink
A path that doesn't exist yet was authorized by walking up to the nearest
existing folder. A dangling symlink fails realpath with ENOENT, so the walk
stepped over it and approved the link's own path; the write then followed
the link and created its target outside the allowed folders. The walk now
continues from the link's target, and returns that real destination.
* refactor(native-chat): rename the chat visuals folder to the chat scratch folder
The per-chat folder agents write into is the chat's scratch space, not only
a visuals folder. Its root becomes <state>/chat-scratch/<hash> (the hash is
unchanged) and the agent variable becomes ORCA_CHAT_SCRATCH_DIR. The old
ORCA_CHAT_VISUALS_DIR is never set and is still stripped from what every
agent child inherits, Pi included. The visuals skill names the new variable
and says the folder is temporary.
* test(native-chat): keep the delivery test to the rename
The launch parity snapshot already pins that every agent drops the
pre-rename variable.
* feat(native-chat): chat scratch folders expire after 30 days unused
A chat's scratch folder was removed only when its record was gone or its
local workspace was proven deleted, so the folder of a long-idle chat
lived forever.
The sweep now expires a chat's folder once its newest journal row and
the newest entry anywhere in the folder (complete, no-follow scan) are
older than 30 days, unless the host holds a running agent, a delivery or
an open dispatch for the chat. The final recheck and a rename to
<hash>.expired run in the chat's session lane and then a per-folder lock
that Orca scratch saves (desktop IPC and host RPC) also take; the delete
runs outside both. A save that settles after the age scan marks that
expiry stale. Leftover .expired folders and the pre-rename
native-chat-visuals root are removed.
The workspace-removed rule and its catalog verdict are deleted.
* fix(files): refuse any dangling symlink in the missing-path walk
Following a dangling link by hand resolved relative targets against the
lexical parent, not the link's real folder: through a folder link the walk
could loop forever or authorize the wrong file. Refuse instead: realpath
can't say where a dangling link leads, but a write would follow it.
* test(files): drop the loop case that duplicated the dangling-link refusal
* refactor(native-chat): trim the scratch expiry and test the real live-work veto
- Remove the other-profile catalog options (running profile id, never-written
profiles) that only the deleted workspace verdict used; history cleanup's
reader is unchanged.
- The sweep keeps its original remove-and-warn block; an unused chat's
folder is parked by one function. Drop the unused `now` option and the
candidate identity check.
- The live-agent case runs against the host's real runWhileExecutionIdle.
* style(native-chat): keep the session host within its line limit after the main sync
* fix(native-chat): every scratch save and every overlapping sweep share the folder lock
Binary (base64) runtime saves now take the scratch folder lock like text saves, and each folder
keeps a set of expiry candidates so one sweep finishing no longer hides a save from another.
* fix(mobile): size markdown table columns to their content
* fix(mobile): always show the copy button under chat messages
Revealing it on tap shifted the transcript.
* fix(mobile): read markdown with a CommonMark parser
markdown-it rather than desktop's micromark: about 8x faster on the phone's JavaScript engine.
* Scope Windows native compiler setup and measure ARM inbox preparation
* Add a controlled Windows native preparation comparison
* Test native preparation through an injected process runner
* Guard reviewed Windows native batch runners
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters
A phone created through a desktop now records whom it acted for, so the
desktop's Hide other-client workspaces filter keeps the user's own phone
workspaces (locally and on servers) and hides only other people's.
The desktop relay marks each server row its sidebar would hide, and the
rows its own browser or chat tabs keep awake; the phone applies both.
* test(e2e): dedupe sidebar row ids in the visibility spec
* fix(mobile): address review: owner naming, old-server relayed phones, scope trim
* refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag
A phone now records its owner as the creator (this host for a directly
paired phone, the relaying desktop for a delegated one), so the shared rule
is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved
browser/chat tabs into hasHostSidebarActivity, so the phone's status dot
follows too, and local worktree.ps counts the same signals. The old-server
fallback is removed.
* test(e2e): poll the phone and the sidebar together in the visibility spec
* feat(native-chat): copy chat selections when Copy on Select is on
The terminal's Copy on Select setting now also copies text selected in a
native chat transcript, at the point the Add to chat offer already treats
a selection as finished. It writes the same text right-click Copy does,
skips the composer and other fields, and leaves the setting key as is.
* fix(i18n): translate the Copy on Select description and search keywords
Settings and Settings search showed English to es, fr, ja, ko and zh users
after the description moved to new keys. Extend the old terminal-only
translations to cover chat, and translate the new highlight and chat search
keywords.
* fix(native-chat): send typing after a click to the chat's composer
The default Claude/Codex chat never wired the root typing redirect, so a
click in the transcript, on a toggle or link, on the chat's own tab, or on
blank app chrome left typed keys with nowhere to go. The chat-view-over-TUI
had the redirect but treated every button and link as a key owner.
Both views now share one hook that runs the reveal focus, focuses the chat
on blank clicks, routes keys typed inside the chat (but not in overlays it
portals out), and, while it is the only chat visible in the focused group,
routes keys typed with focus on the page body. Buttons and links pass
letters but keep Space and Enter. Re-activating an already visible chat tab
re-runs the composer claim, as a terminal tab refocuses xterm.
* fix(native-chat): refocus the composer when its active tab is activated again
Re-activating an already visible chat tab re-ran the ordinary reveal
claim, which stops as soon as focus is anywhere inside the pane. With the
transcript or a code-block Copy button focused, the tab shortcut left focus
there and Space kept scrolling or pressing the button. The activation now
moves focus to the composer from inside the pane too, unless a text field
such as the find input holds it; an ordinary reveal still keeps the user's
focus.
Each Tasks sheet was its own native modal, and iOS refuses to present one
over another that is still showing. They now share one modal and stack by zIndex.
Fixes#21003
* feat(wsl-mover): shell history moves with the project to the WSL host (D1b-6c-1)
* fix(wsl-mover): moved WSL history lands under the host's login shell (D1b-6c-1)
* fix(wsl-mover): history follows the shell a pane would actually run, fallback included (D1b-6c-1)
* fix(wsl-mover): history installs follow the real pane shell, wait for live shells, and mark what they installed (D1b-6c-1)
* fix(wsl-mover): history installs wait until live shells can be checked (D1b-6c-1)
* feat(relay): log what the sticky slot spends its hold on, per pooled connection
* fix(relay): time every sticky round trip at the PostgreSQL client, pool wait apart
* docs(relay): say which round trips the sticky slot tally covers
* fix(agent-launch): find a repo's project the way the window does
The host now also takes a project named after the repo as that repo's project, as the window
does, so both pick the same project row and runtime even when a repo was moved out of the
project named after it.
* refactor(agent-launch): share one repo-to-project match with the window
The host's agent-launch lookup and the window's launch preflight now call one projectMatchesRepo
instead of each writing the match out. No behaviour change on either side.
* feat(relay): watch a reserve cell and flip it back on the first step 5 stop rule
A manual, inert-until-dispatched workflow samples one reserve cell every minute for up to
300 minutes against the step 5 §9 stop rules, logs one JSON line per sample, and on the
first trip (or N unreadable samples in a row) runs the flag tool's own admitMode=db write
under the flag workflow's concurrency group. dry-run logs the flag tool's plan instead.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): keep the reserve watch's flip back out of the flag group, and baseline 503s before the flip
- The flip back holds no concurrency group: GitHub keeps one pending run per group and a
newer one cancels it. The flag tool's CAS orders it instead; a generation conflict is
re-read and retried (5 tries, backoff) while the switch file still says reserve.
- The 503 background comes from the 10 minutes before a required flipped-at, or an explicit
value; neither is refused.
- A failed or cancelled write-mode watch flips back too. An unreachable cell fails with the
exact break-glass inputs.
- Missing 503 data (empty series, unpublished minute, unparseable time) is unverifiable.
- Seat-behind-row key adds the director instance and cell; six segments share one command.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): read the reserve watch's 503 publishing front from every response code
A quiet fleet has no 503 series, so the watch read it as unverifiable and flipped a healthy
cell back within minutes. The 503 read now groups request_count by response code in one
query; the newest minute any code has a point for is how far the series is published, so a
503 minute at or before it with no point is zero and minutes past it are left out.
Also: the flip-back job's steps run on always(), flipped-at ages against the watch's start
in later segments, a pending pre-flip background no longer adds to the unverifiable streak,
the flip back retries runtime-status before calling for break glass, and the concurrency
rationale lives once in the docs.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* docs(relay): state what a cancel during the flip-back lease wait can leave, and the 503 publish-skew assumption
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): fail a segment closed on a corrupt watch state file, and keep no checkout token on disk
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): alarm on a failed flip back, refuse a watch under a rollout lease, wait for reserve to apply, and control the duplicate read
- A flip back whose write or plan never finished (no ID token, a failed login, a silent cell)
raises an ::error:: annotation and a FLIP BACK FAILED summary, as the clock-skew monitor does.
- The first segment reads the Cloud SQL rollout lease through the lease action's own reader and
refuses to start while any run holds it, or when it cannot be read.
- A cell not yet reporting reserve is waited on for up to 10 minutes, judging nothing, before the
watch ends refused without a write.
- The duplicate-seat read takes the per-minute ledger summary in the same query and window; with
no fresh summary in it the sample is unverifiable, not zero duplicates.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): page on a refused reserve watch or a failed flip back
A refused watch can leave a cell in reserve and unwatched, the same as a flip back that did not
land. Both now raise an escaped ::error:: annotation through one `alarm` command, and in write
mode write orca_relay_reserve_watch_alarm with gcloud logging write. A new log-based metric and
alert policy page on any such line, per cell, through relay_alert_notification_channels, and the
deploy identity gets roles/logging.logWriter in production to write it.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): name the log-writer risk, check the alarm's project, and say a failed login cannot page
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* docs(relay): note a force-cancel can skip the reserve-watch flip-back write
The drag preview's set-and-restore of the active tab per group now goes
through previewDragActiveTab, restoreDragActivationSnapshot and
restoreSourceGroupActiveTabAfterDrop instead of setState from a component.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Worktree removal, purge, rename, activation wake and SSH target cleanup now
build their tab/group/pane layout changes with named transitions in
tabs/worktree-layout-lifecycle.ts, folded into the same single store update.
Project removal drops its layout writes: purgeWorktreeTerminalState, run just
before in the same call, already cleared every one of them.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(terminal): park and quit capture keeps the stored pane layout
The capture that runs on quit, sleep and park rebuilt the whole pane tree
from the DOM. The tab's stored layout already follows every pane change, so
the capture now writes only what it alone knows onto that layout: the
scrollback buffers, the pruned scrollback refs, and the live PTY bindings
with the focus repair that goes with them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(terminal): keep a stored leaf's scrollback and binding while its pane is unmounted
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- ko Worktree Jump Palette: render "actions" as 동작 so it is distinct from
the tasks entry (작업) in the same list.
- zh DevTools cookie instruction: clarify the filter phrase to
`任意包含 remains 的请求`, keeping the literal `remains` request name.
Scan 2026-10-14: fill target-locale coverage for PRs merged since the
2026-10-13 scan (#28486). Adds the missing keys (es 98, fr/ja/ko/zh 125
each), updates the two changed WorktreeJumpPalette strings, and fixes
pre-existing mistranslations: the ZCode missing-TUI notice (English in
all five locales), fr settings search keywords (procuration/indigene),
zh workspace-cleanup browse MT, 6 zh keys that still used Latin "Agent",
and the zh AccountsPane credential-help cluster (leftover English and a
mangled __Host-console_session token).
Leaves the 11 pinned provider-credit-balance keys plus the 3 pinned Chat
UI keys as English fallback by design. Records the scan in
config/update-translation.txt.
* fix(ssh): the SSH password dialog no longer closes itself and comes back
A host that drops an unanswered login no longer replaces the open prompt
with a new one, a background dial no longer replaces a connection whose
reconnect is asking the user, one Cancel closes every open prompt for that
host, and a paired client's automatic connect stays automatic.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ssh): a typed password its reconnect has yet to use is not replaced by a background dial
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
* Avoid unused module imports and trim cosmetic test coverage
* Capture hidden popup frames and attach CDP before guest creation
* Overlap relay test projects within the existing worker budget
* Consolidate native preparation and add an opt-in transform cache comparison
* Release inherited debugger sockets during final E2E teardown
* Refresh unit scheduling weights from all five qualified ARM shards
* Avoid retaining unasserted RPC call history in the delivery model
* Use the supported unconditional background wait syntax
* Overlap unconditional output upload with native preparation
* Verify relay integration stays enabled for ordinary unit callers
* fix(terminal): bring each pane back after a terminal service restart when it is shown
After Settings -> "Restart terminal service", every pane that was open kept its
dead process: typing did nothing and agents never resumed until Orca relaunched.
A restart-replaced exit now arms the pane: the visible pane spawns its
replacement at once, any other pane when it is next shown, through the same
fresh spawn and cold-restore agent resume the hibernation wake uses, so old
output stays above the new prompt. The exit keeps the agent status the resume
reads, and clears the dead binding so the replacement binds as a first spawn.
Local spawns wait for an in-flight restart so they reach the new daemon.
Claude-Session: p2-headless
* fix(terminal): resume from the live recovery record; skip a pane another flow rebound
A restart exit is a process death to main's agent-status store, which clears the
pane's row before the window sees the exit, so keeping the window's copy changed
nothing. The resume reads the live recovery record every agent turn already
writes, as a relaunch after a crash does. The replacement also stands down when
the pane's stored binding already names another process.
Claude-Session: p2-headless
* fix(terminal): send every restart exit down one path
A finished-turn agent pane took the hibernation wake, which gives up when its
record changes before the pane is shown. Every restart exit now arms the
replacement directly.
Claude-Session: p2-headless
Ten create sites now mint the new tab's id (or browser page id), pass it to
the create action, and key every follow-up (select, focus, tab order, group
focus, remote page handle) off that id instead of the returned record.
Creates stay synchronous, so the follow-ups still run immediately.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A workspace with no groups got a random root group id from the window. It
now gets the name-based id the shared loader already mints for that
workspace's group, so main can derive the same id without the window.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(wsl-mover): a moved pane reopens in its last folder on the WSL host (D1b-6b)
* fix(wsl-mover): consume a restore hint only when its pane spawns, scope it to its worktree, retry failed installs (D1b-6b)
* feat(orcad-migration): a capability-gated blob channel beside scrollback, for the WSL mover's new data (D1b-6a)
* fix(orcad-migration): the commit names its blob set, prune clears leaked blobs, reserved names refused (D1b-6a)
* feat(relay): split step-5 shadow compare and dry-run logs per cell
The 24 h per-cell shadow gate needs per-cell sticky-compare counts, per-cell
unexplained counts, and dry-run `full` refusals while the map showed room.
Both summary lines gain additive per-cell fields; existing fields are unchanged.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): count a cell-mismatch on both cells and bound per-cell maps by the fleet
- countByCell (was cells) counts a compare on every cell it involves: the
database's cell, every seat's cell when there is no row, and both cells
for a cell-mismatch. Drop the 64-cell cap and cellsOverflow.
- Add ReservePlacer.free and use it for the dry-run free share.
- Rename the room bound to RESERVE_DRY_RUN_ROOM_BOUND.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): count a duplicate-seat compare on every seat cell
An unexplained duplicate seat counted only on the database's cell, so a cell
whose stale feed kept a second seat could pass its own step-5 check. Count it
once on every seat cell, the same as a cell-mismatch.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): count explained cell-mismatch and duplicate-seat on the database cell only
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): key per-cell counts on prototype-free records
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): explain a duplicate seat left by a just-superseded half-open control
When the database names the newest seat exactly, every other seat has a
strictly lower epoch, and the newest seat joined within the cell's silence
close (timeout + one ping interval + the map lag bound), the older seat is
a half-open control awaiting its 75 s pong-silence close, not a fault.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): bound the superseded duplicate-seat rule on the move's age
A same-epoch rejoin resets joinedAt, so a stale older seat could stay
explained forever; the director-clock sighting of the epoch moves only
when the epoch rises.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): leave a superseded duplicate seat unexplained without an epoch sighting
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): explain a superseded duplicate seat only from a prior sighting
compare() recorded the epoch before classifying, so a first-ever sighting
(restart or eviction) read as a fresh move. Classify first and take the
move's age from the record that existed before this compare.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): also bound a superseded duplicate seat on its join age
A long-ago lower-epoch sighting dates the move to this compare, so the
current seat's join age bounds it too.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): count an ageless superseded duplicate seat as its own class
A superseded-shaped duplicate-seat with no prior sighting of the host (the
first compare since a restart) has no move age, so it is now
`superseded-unknown-age` (explained: false) rather than a plain duplicate.
The count line also carries `uptimeMs`, so the gate can discount that
class in an instance's first 100 s.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* fix(relay): limit superseded-unknown-age to an instance's first 100 s
Flushes are lazy, so a window has no upper bound and an uptime field on the
count line cannot date it. The compare now records when it started and
emits superseded-unknown-age only within the superseded close bound of
that; later, a missing sighting means another instance served the host or
the sighting was evicted, so it is a plain unexplained duplicate-seat.
Drops the uptimeMs field.
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* docs(relay): say where a missing sighting comes from after the first 100 s
Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
* perf(ci): reduce test overhead and reuse expensive build work
* test(ci): keep worker startup outside failure-case deadlines
* test: isolate push fixture advisory locks
* test(ci): preserve fixed drain retry allowance
* Align CI contracts with current triggers and guide content
* Stabilize consolidated reveal scenarios and retain behavioral CI contracts
* Fix upstream lint blockers while retaining behavior and assertions
* Classify global Jira preferences in the WSL migration inventory
* Align Jira test fixtures with startup and build safety checks
* Include package-test routing dependency in CI checkouts and fixtures
* Keep native stream classes intact in transcript dependency tests
* fix(terminal): keep a hibernating pane's resume record through its stop
The agent dies before its shell, so the shell's command-finished mark read
as the agent exiting and deleted the record the hibernation had just written.
* fix(automations): sleep a finished run's terminal instead of closing it
Closing it left the run's own workspace empty, so opening it later showed a
blank chat or shell instead of the run's conversation.
Fixes#28645
* feat(chat): Resume in New CLI for OpenCode and OMP chats
OpenCode's and OMP's CLIs have no copy flag, but their chat protocol copies a
conversation (ACP session/fork). For a fork of one of their chats, the chat's
host now has a short-lived agent, launched exactly as the chat's own, copy the
conversation; the terminal then opens the copy with a plain resume. The chat
keeps its own conversation, so the CLI is never a second writer on it.
The copy travels on the existing aiVault.prepareSessionResume fork request as
an optional forkedSessionId; a host that predates it answers without one, and
the client asks for an Orca update instead of resuming the original.
* fix(chat): open an OpenCode 2.x chat's CLI copy with opencode, not opencode2
The copy kept the row's agent, so Resume in New CLI ran `opencode2 --standalone --session <copy>`,
which OpenCode 2.x doesn't install. The chat's host now answers the copy with the agent whose CLI
opens it: the chat's own.
* fix(chat): type the copied conversation's agent as a history agent
* fix: preserve independent chat copies through CLI launch
* test: keep only the tests that pin user-visible behavior
* fix(chat): validate OpenCode copies on their execution host
* test(chat): expect copied OMP forks after main integration
* Run safe worktree cleanup on dev and add interactive pnpm reclaim
* Rename storage cleanup command to reclaim-space
* Keep real stream constructors in transcript dependency tests
* Detach cleanup output and align historical cache previews
* fix(terminal): reject a requested tab id that is already taken
createTab silently minted a fresh id when the caller's requested id was
already in use. The caller's PTY routes agent status by the id it asked
for, so the new tab never received its agent status and the status went
to the other tab instead. Now the create throws and changes nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(agent-launch): drop the unreachable tab id mismatch guard
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(mobile): log OTA shell lifecycle to the connection diagnostics
Exported connection logs showed nothing about the web shell, so a phone
stuck on "Opening workspace" could not say whether it stalled activating
the download, loading the page, or waiting for its first paint. Record
sparse shell milestones, page signals, one-shot 15s stall warnings and
bridge diagnostics into the per-host connection log. Diagnostics only.
Refs STA-10545, #28626
* fix(mobile): cut shell log volume and pinpoint each waiting screen
Every page route mounts its own shell, so four lines per healthy open
evicted connection history from the 200-entry log. A healthy open now
writes one line, on paint, carrying source, build and elapsed.
The 15s stall line now also covers "Checking host" (gate verdict and
whether the cache read answered) and a download with no progress. The
"page has not drawn" line counts the page's ready asks and whether the
document loaded, telling a page that keeps refusing init from one that
took it and died. Frames dropped with no bridge host, an unreadable
host profile, post-failed error names and route-refused issues are now
logged; path masking catches quoted, bracketed and Windows paths. The
exported report header names the shell build and bridge protocol.
Diagnostics only: no behaviour, UI or wire change.
Refs STA-10545
* fix(mobile): dedupe flapping shell lines and mask spaced paths
A host with no cache flapping between unreachable and dialling wrote
"checking host" and "host unreachable" every cycle per mounted shell;
each is now written once per host until a different shell line lands.
Path masking keeps going across a space while the next word still holds
a separator, and accepts a comma before the path. The paint line's
elapsed time is renamed activatedMs, which is what it measures. A
route-only session rebuild re-arms the stall timer.
Refs STA-10545
* refactor(mobile): split the shell log module by responsibility
shell-lifecycle-log.ts passed the 300-line lint cap. The line type,
recorder and flapping dedupe move to shell-lifecycle-line-recorder.ts,
bridge diagnostic lines to shell-bridge-log-lines.ts, and user-path
masking to shell-log-path-masking.ts, with tests split to match. No
behaviour change.
Refs STA-10545
* Skip reindexing unchanged transcripts on cold session list reads
A cold session-list read re-parsed every self-contained transcript and rewrote its search rows, even when nothing had changed. That wasted work and churned the index on every list.
- When a transcript's stat (mtime, size, inode, device) matches a complete index entry, the list read now skips the rewrite. Agents with sidecar metadata, unknown sizes, or partial or stale entries still reread.
- Test fixtures that replay a "changed" transcript now pass a new mtime so they still exercise the replace path.
- New tests cover unchanged skips, changed size/mtime/file identity, sidecar refresh, due/failed repair, and append.
- Dependent-transcript and Codex title tests check that cold rereads keep search and title data current.
* Make the self-contained transcript check exhaustive over every agent
- Replace the hand-maintained `Set` of self-contained agents with a
`Record<AiVaultAgent, boolean>`, so adding an agent type fails typecheck
until someone decides whether its transcript is self-contained.
- Classification is unchanged: the agents that were in the set are `true`,
and the rest are `false` with a short note on the sidecar or metadata
source they depend on.
- Make the replace-generation tests derive the second mtime from the first
candidate instead of a hard-coded timestamp, so they stay valid if the
default fixture mtime changes.
* Guard file-stat reuse with a self-contained transcript test
Indexing skips reparsing unchanged transcripts for agents marked self-contained. That is only safe if their parsers read nothing but the transcript itself, and nothing enforced it. A parser that started reading a sidecar or dependency file could serve stale index entries.
- Export SELF_CONTAINED_TRANSCRIPT_AGENTS so the test checks the real list
- Add a test that parses each self-contained agent's fixture, both direct and cached, and fails if any file other than the transcript is read
- Register the test in the SQLite runtime-file include list
* Require stored file identity before skipping a transcript rewrite
- Rows with a NULL dev or ino were treated as covered, so a file replaced with the same size and mtime could be skipped and never reindexed.
- Such rows now count as uncovered, so the rewrite runs and records the identity.
- The identity is read through the shared `fileIdentity` helper instead of the raw file fields.
* Add regression test for session rows indexed without file identity
A session row indexed before file identity (dev/ino) was recorded was never reread once the stat began supplying one, so it could keep stale search content. This test covers that case: the first read with identity replaces the row, and a second read with the same identity leaves it alone.
* test(session): compare what the window shows before and after the shared session loader
Boots the window's real startup hydration on each saved-profile fixture as
stored and as the shared loader rewrites it (load then save), and compares
tabs, groups, active tabs, pane trees, terminal bindings, editor and browser
tabs. Today's eight differences are listed with the loader rule behind each;
the list may only shrink, so any new difference fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(session): let the parity test own its store so the harness reads no host focus
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(session): restore the parity boot's UUID spy even when the boot throws
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Restarting the terminal service ends each session with a synthetic -1 exit. Main
read those as ordinary deaths and retired every terminal surface from the saved
layout, while the window kept drawing them. Phones and the CLI lost the tabs, and
the next launch restored a split as a single pane.
The restart now labels those exits as replaced, and main records them in its
existing intentional-stop register, the one terminal restarts already use. The
runtime keeps the surface and the window receives the same replaced label.
Claude-Session: p2-headless
Seven call sites created a terminal tab and then queued its startup command
in a second store write. Pass the command through createTab's existing
pendingStartup option instead, so the tab and its command land in one write.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The dialog title and cancel button said 掃除 (household cleaning), while its own description and confirm button already say クリーンアップ. Use クリーンアップ throughout.
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>