Commit Graph
10043 Commits
Author SHA1 Message Date
Jinwoo-H 145e7f0c29 Merge remote-tracking branch 'origin/main' into mobile-rearch 2026-09-01 22:08:48 -04:00
Jinwoo-H 6840ba0b7f fix(mobile): merge the duplicate session-panel-host import
oxlint's import/no-duplicates (deny-warnings in the native-plugins gate) flagged the split import.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 22:08:48 -04:00
Neil 8b7d778a2e perf(git-common): bound the fs-stat fan-out in the worktree pollers (#17839)
* perf(git-common): bound the fs-stat fan-out in the worktree pollers

snapshotGitCommon and snapshotBase issued one fs op per candidate via
Promise.all/a serial loop, unbounded by worktree count. At 973 live
worktrees this queued ~6,800 concurrent stat calls (measured peak 6000
in a 1000-entry synthetic benchmark) onto libuv's 4-thread default
pool, starving every other main-process fs operation for the scan's
duration (~1s). Bound both to concurrency 8 via the existing
forEachWithConcurrency helper, matching the precedent in
exact-ref-probe.ts and worktree-head-identity-reader.ts. Peak
concurrent stats dropped 6000 -> 48 in the benchmark; wall time was
essentially unchanged (495ms -> 541ms), since the real bottleneck was
never total scan time but pool starvation of unrelated work.

Also make the no-native-watch and crash-fuse polling fallbacks in
worktree-git-common-watch.ts / worktree-git-common-narrow-watch.ts
self-calibrate their cadence: on platforms/paths where this poller is
the sole change signal, a fixed 2s cadence at hundreds of worktrees
approaches a permanent scan loop. Stretch the interval so a scan stays
a bounded fraction (10%) of its own cadence, capped at 30s, floored at
the configured base interval. Left the reconciliation backstop (fixed
30s cadence, already accepted) and checkPendingMarkers (bounded by
concurrent-worktree-creation count, not total count) untouched.

Fixes #17828

* perf(git-common): split the tripwire from the per-entry sweep cadence

Review on #17839 found a real staleness trade-off: adaptiveCadence
gated ALL detection (worktree add/remove, HEAD, dirty refs, AND
per-entry commit signals) behind one stretched interval, so on the
crash-fuse polling fallback the reviewer measured cadence sitting at
5.4-10s sustained and hitting the 30s cap once a single scan reached
3s at 973 worktrees -- worse than the pre-#17828 fixed ~2s+250ms
baseline for signals users notice immediately (sidebar worktree list,
branch labels).

Split snapshotGitCommon into a cheap structural "tripwire" (readdir,
worktreesDir signature, primary-file signatures, newly-appeared
entries -- ~5-6 fs ops, O(1) in worktree count) that always runs on
the fixed pollIntervalMs, and the O(n) per-entry sweep (commit/dirty
detection) that alone is gated by the adaptive cadence via a
nextSweepDueAt deadline. Existing, unchanged entries are carried over
by reference on a tripwire-only tick (no re-stat), so diffing produces
no spurious events; genuinely new entries are still stat'd immediately
so worktree add remains real-time. This keeps everything on one
ticking-flag-guarded loop (no new concurrency/race surface) --
scheduling stays fixed at pollIntervalMs; only nextSweepDueAt stretches.

Also drop the adaptive-cadence seed heuristic entirely: nextSweepDueAt
starts at 0, so the first regular tick after bootstrap sweeps
unconditionally on its own schedule instead of guessing an initial
interval from the bootstrap snapshot's duration (which could stretch
the very first tick to 10-30s on a slow disk).

Documented that worktree-git-common-watch.ts's adaptiveCadence call
site is unreachable in production (Electron only ships
darwin/linux/win32, both covered by NARROW_WATCH_PLATFORMS) rather
than implying it protects real users. The reachable path is the
narrow-watch crash-fuse fallback in worktree-git-common-narrow-watch.ts.

Filed #17878 to track the real long-term fix: periodically retrying
the upgrade back to the narrow watch after a crash-fuse trip, so the
degraded/polling state doesn't need to be tuned at all once the
underlying failure clears.

* perf(git-common): gate per-entry structural stats on the entry-dir signature

Every real git write inside a worktree admin entry (HEAD, index,
config.worktree, locked) goes through a lock file + rename, which moves
the entry directory's own mtime/ctime/size signature. Only `gitdir`
(worktree move/repair) is rewritten in place, and that's already covered
by the periodic ungated backstop (INDEX_BACKSTOP_TICKS). The previous
comment claiming structural leaves "change in place every tick" was
wrong; verified against git 2.55 across checkout, commit, amend, reset,
ref updates, stash, worktree lock/unlock, config --worktree, and index
writes.

Gate all six per-entry stats behind the entry dir's own signature instead
of stat-ing every leaf unconditionally every tick: an unchanged entry now
costs one stat per tick instead of six, and a changed one still costs six
(bounded by change rate, not worktree count). This also fixes the actual
in-flight fan-out: forEachWithConcurrency(entries, 8) previously still
issued 6 stats per in-flight entry (48 real concurrent ops); with the
gate, warm ticks issue ~1 stat per entry, so true in-flight tracks the
concurrency limit directly.

This makes the follow-up adaptive-cadence machinery from the prior commit
unnecessary: the crash-fuse and no-narrow-watch polling fallbacks no
longer need to stretch their own cadence, since a warm sweep across
hundreds of worktrees is now cheap regardless of interval. Revert both
call sites to a fixed pollIntervalMs and delete the adaptive-cadence
option, the split tripwire/sweep cadence, and the seed heuristic — none
of it earns its complexity once the real per-entry cost is fixed at the
source. Per-entry staleness on the crash-fuse path returns to a fixed 2s
+ 250ms debounce instead of the previous 5.4-30s adaptive stretch.

Refs #17828
2026-09-01 19:04:03 -07:00
Neil e89321192a perf(worktree): batch remote conflict probes, re-arm the prepared checkout (#17829)
* perf(worktree): batch remote conflict probes, re-arm the prepared checkout

A repo with many remotes paid one `git show-ref --verify` subprocess per
remote on every branch-conflict check during create. Ask one
`git cat-file --batch-check` over stdin instead; it reports a missing ref
as data rather than a failed exit, so a batch stays as decidable as the
per-ref probe. Hosts that cannot feed stdin, and undecided batches, still
fall back to the per-ref path.

The prepared checkout was single-use, so the second create in a row paid
the full cold `git worktree add`. Re-arm it in the background after one is
consumed; the existing TTL and preparation limit still bound it.

The create timing recorder existed but its phases were never emitted and
did not cover preflight, leaving a multi-second gap in the trace with no
attribution. Add `resolve_name`/`prepare_push_target` phases and record the
breakdown, plus the unattributed remainder, on the create span.

* fix(worktree): format the conflicting review number eagerly for the create error

* perf(worktree): re-arm a prepared checkout only for a burst of creates

Re-arming after every consumed preparation spends a full checkout and
~200MB of disk on a user who created one worktree and stopped, then pays
an unexplained delete when the TTL expires five minutes later. Track when
each preparation key was last consumed and only replace it when a second
create lands inside the burst window, so the warm second create is still
free and an isolated create costs nothing.

* fix(worktree): address review findings on the create-path batching

Three findings from PR review:

The `batched.found` fallback in the remote-conflict probe was unreachable
— a present ref is decisive, so `found` never survives with `unknown`
set, and the guard above already returns that case.

`rearmPreparation` checked for an existing preparation before recording
the consume, so a prefetch that re-armed the key while create finalized
swallowed the timestamp and made the next create look isolated when it
was really mid-burst.

Create runs some phases concurrently, so summing phase durations
double-counted overlap and understated `unattributed_ms` — the one
number that matters when a create is slow for no visible reason. Measure
the union of the phase intervals instead.

* refactor(worktree): move stale-preparation cleanup into its own module

The preparation module crossed the 300-line budget. Crash recovery is a
separate concern from the pool itself — it discards preparations another
process left registered, single-flighted per repo and runtime so a burst
of arming calls shares one worktree listing.

* test(worktree): make the re-arm test able to fail

The burst test armed a preparation manually after the second consume, so
the third checkout appeared whether or not the re-arm produced it — the
assertion passed with re-arming disabled. Drop that arming call so the
third checkout can only come from the re-arm, and assert the consume
results rather than discarding them.
2026-09-01 19:01:59 -07:00
Neilandkaluli123123 3f5c54332d fix(github-project): sort and group empty field values last in both directions
compareSort early-returned 1 for a missing value — before the trailing
DESC flip — but expressed the same idea as `cmp = 1` for an empty
users/labels list, which that line then negated. Descending order
therefore scattered empty cells across both ends of the table.
getFieldValueForGrouping had the matching defect: an empty list fell
through to deriveStringValue and produced a blank-label group that the
header renders as the literal "All".

Both paths now share one predicate, which also covers `text: ''` and
`date: ''` — reachable because the view normalizer maps a null GitHub
text/date to the empty string.

Co-authored-by: kaluli123123 <295758798+kaluli123123@users.noreply.github.com>
2026-09-01 19:01:45 -07:00
Jinwoo Hong fd33f9b0f9 fix(review-notes): classify send failures and mirrored tabs (#18023)
* fix(review-notes): classify send failures

* fix(review-notes): honor structured runtime error codes

* test(review-notes): use full runtime error envelope

* chore: remove unrelated merge formatting

* refactor(review-notes): share runtime failure codes

* fix(review-notes): classify structured runtime timeouts
2026-09-01 21:52:36 -04:00
Jinwoo-H 1dbbff090a test(e2e): give the colliding-label checkout test the sibling's 120s budget
Under full-suite load it extracts two full trees and took 46s against the
30s config default; its rival-process sibling already runs at 120s.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 21:25:53 -04:00
Neil 401664298f fix(preload): make a dropped bridge key a compile error
The split silently dropped jira.searchUsers and
runtimeEnvironments.retryControlConnection. Neither failed typecheck: the bridge
modules carried no satisfies annotation and the composed api object was
unannotated, so a missing key was only a runtime TypeError in the renderer.

Annotates each module against PreloadApi, the type window.api is already
declared as, so the contract supplies the shape rather than a parallel copy.
Deleting jira.searchUsers now fails with TS2741 naming the key.

Turning this on surfaced 106 places where a bridge locally annotated
Promise<unknown> or unknown[] over a contract that declares concrete types --
the bridge was erasing types the renderer relied on. Those annotations are gone.

Also exposes app.awaitBeforeUnloadCheckpoint, which was declared and called but
never actually on the bridge, so the lazy-chunk recovery reload optional-chained
to a no-op and navigated without joining the checkpoint. The missing key was
caught by the new annotation rather than by hand.
2026-09-01 18:24:49 -07:00
Neil 894c5fe36a test(orchestration): fail loudly on an unexpected second detection call
The mock overwrote resolveDetection on every call, so a second invocation would
strand the first promise and hang to a 30s timeout instead of naming what
changed. A test that hangs rather than fails is how a real bug gets mistaken for
infrastructure noise.
2026-09-01 18:22:59 -07:00
Jinwoo-H 5735189af7 test(mobile): fix hosted iOS e2e harness and root test triage
Hosted iOS security e2e (three branch-only harness bugs):
- `simctl privacy grant` terminates the app; the relaunch cold-resumes
  straight onto the session route, so skip workspace-row activation when
  the hybrid handoff already landed on /session/. Activation failures now
  name the document href.
- Uploaded-path regex fused two abutting orca-paste-*.png paths; use a
  lazy segment quantifier.
- Photos orders by capture date, which addmedia takes from file birth
  time; stage a freshly written fixture copy (copyFile clones birth time
  on APFS) so "last Photo" is the fixture.

Root tests:
- Allowlist walker skips -test-fakes / -test-fixture(s) / .test-support
  sources, which are not mobile call sites.
- Drop the height-only OSC-8 test: main's #17759 clears restored ranges
  on any dimension change and pins it.
- Drop serve-update-handoff-startup-order.test.ts: main pins the same
  order against main-process-preflight.ts.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 21:17:38 -04:00
Neil 4efc86a33c feat(app): open Markdown files from the OS in the floating workspace (#17906)
* feat(app): open Markdown files from the OS in the floating workspace

Registers Orca as a Markdown handler on macOS, Windows and Linux, and opens
an OS-handed .md/.markdown/.mdx file as a floating-workspace editor tab —
the one editor surface that needs no project. Works cold-start and when
Orca is already running.

Main buffers the paths and both pushes to a live renderer and answers a
pull on renderer mount, mirroring SkillShareDeepLinkState. The buffer is
only released once delivery is possible: the renderer's pull is what proves
its ui:openMarkdownFiles listener is attached, because a push into a window
whose renderer has not subscribed is dropped by Electron with no error. Both
the push and the pull restore an undelivered batch, and a renderer reload
clears the latch so the fresh renderer re-proves itself. Paths are stat'd and
proven to be files before authorizeExternalPath sees them.

Windows association is registered by hand in the NSIS include rather than
through electron-builder's `fileAssociations`: app-builder-lib emits
APP_ASSOCIATE, whose first line overwrites Software\Classes\.md's default
value with no backup — silently taking .md from whichever editor owns it,
for every existing user on their next update — and APP_UNASSOCIATE never
restores it. The hand-rolled registration is additive (ProgID +
OpenWithProgids + SupportedTypes) and leaves the user's default alone;
verified end to end on a real Windows 11 host.

Co-authored-by: Wooseong Kim <innocarpe@users.noreply.github.com>
Co-authored-by: Jaydev <java-jaydev@users.noreply.github.com>

Closes #10138

* fix(os-open): register the new listener in the IPC inventory, and guard a non-array payload

CI caught two things the local run did not.

useIpcEvents-lifecycle.test.ts is an inventory of every App-lifetime IPC
listener and the exact order they register in; ui.onOpenMarkdownFiles now
appears there, positioned after the workspace-shortcut bridge's last
listener, which is where it actually registers.

Chasing that failure surfaced a real gap: the pending-open payload crosses
the preload boundary, so a stale or mismatched preload can resolve with
something that is not an array, and reading .length off it threw inside the
promise chain instead of failing at the boundary. Array.isArray now gates it,
with a regression test.
2026-09-01 17:35:38 -07:00
Neil d48ab96144 test: stop two suites failing for reasons unrelated to their subject
The zsh wrapper test relocated into a fixed-name directory in shared temp, so a
single killed run left it behind and every later run on that machine failed with
ENOTEMPTY, permanently. Makes the name unique while keeping the non-ASCII
component the test exists for.

The palette budget asserted a helper named percentile95 that returns
sorted[floor(n * 0.95)] -- the maximum of the batch. Asserting worst-case
wall-clock under a parallel runner measures scheduler preemption: the asserted
quantity ranged 123-343ms across 20 saturated windows and blew the 220ms budget
in 6 of them, while the fastest sample of those same batches held at 19-32ms.
Asserts the fastest sample instead and adds a deterministic fan-out ceiling, so
the guard counts work rather than time. Budgets are unchanged.
2026-09-01 17:21:10 -07:00
Neil f2fa4a7754 fix(worktrees): drop an unreachable runtime arm from the retirement gate
`findExactRepoOwner` already refuses a repo carrying both a runtime
`executionHostId` and a `connectionId` -- `resolveRepoOwnershipEvidence` calls
that pair contradictory, and one non-owned candidate voids the whole lookup.
There is also no way for a `connectionId` to yield a `runtime:` host id, since
`toSshExecutionHostId` always emits `ssh:`. The runtime arm of
`connectionMatchesHost` could therefore never decide anything, and the test
meant to pin it was passing through the contradiction gate instead.

Keep the SSH arm, which does gate, and record where the runtime refusal
actually comes from. Unreachable code on a destructive path reads as a
guarantee it is not making.

Refs #17776
2026-09-01 17:20:17 -07:00
Neil 398aeccdfe fix(worktrees): retire runtime-host metadata a scan proved gone
A paired client's WorktreeMeta for a runtime host is exempt from
gcStaleWorktreeMeta -- that GC skips any row that is not local on both the
repo and the meta's hostId -- so a scan-proven removal is the only thing that
ever retires one. Both halves of that path were gated to `ssh:`, so the client
kept a row for every remote worktree it had ever seen and dropped none.

The renderer already computed the removals for runtime hosts and purged its
own in-memory state with them; only the persisted half bailed. Widen it, and
the matching main-side handler, to runtime hosts. `OffHostExecutionHostId`
names the set precisely: the hosts the local-only GC skips.

Also require `source === 'git'` before retiring anything. `session-fallback`
reports `authoritative: true` but is the truncated, visibility-filtered
`worktree.list` reply from a host too old for `worktree.detectedList`; its
omissions are no evidence a checkout is gone. That guard did not matter while
this only ran the in-memory purge, and does now that it deletes rows.

A repo that reaches its checkouts over a connection is still never condemned
under a runtime host id -- the host that executes owns that verdict.

Refs #17776
2026-09-01 17:20:17 -07:00
Neil 05a7d39058 test(runtime): make the off-host sweep case a real control
The row was stamped `ssh:build-box`, which
`captureNativeLocalWorktreeMetadataScanExpectation` filters out before the
prune runs -- so it survived whether or not any host gate existed and pinned
nothing.

Stamp it `local` so it is a genuine prune candidate whose directory really is
missing, and make the fixture identical to the first case apart from
`connectionId`. That pairing is what proves the behavior: the same fixture
without a connection loses the row. Deleting any single gate would not show
it, since four independent checks derive from `connectionId` on this path.

Refs #17776
2026-09-01 17:20:17 -07:00
Neil ff8b4d08ab fix(runtime): sweep missing local worktree metadata on the host that owns it
`pruneMetadataMissingFromAuthoritativeLocalScan` had exactly one caller:
`ipcMain.handle('worktrees:listAll')`. A headless runtime host has no
renderer, so it never ran, and that host's `worktreeMeta` grew without bound
even for its own local repos -- 129 of 139 rows dangling on the profile in
#17776.

Run it from the runtime's own detected listing instead. That is the same
trigger on the same evidence: `listDetected` already prunes lineage on an
authoritative scan, and a paired client refreshing a remote repo calls
`worktree.detectedList`, so the host now sweeps exactly when the desktop
would have.

The expectation is captured before the scan, because listing can mutate
metadata synchronously before its first await. WSL-routed repos are excluded
for the reason the desktop listing excludes them: the listing runs in the
distro and reports Linux paths while metadata can hold UNC ones, and v1
cannot prove those aliases equivalent. A runtime needing repair throws rather
than resolving routing, which is likewise no basis for deleting rows.

The prune's own gates still apply, so an SSH- or otherwise off-host repo is
never swept from a local stat -- the execution host owns that verdict.

Refs #17776
2026-09-01 17:20:17 -07:00
Neil 1a11f82fcc test(persistence): cover each session scalar as an orphan's only residue
`activeWorktreeId`, `activeWorkspaceKey` and `activeWorktreeIdsOnShutdown` are
pruned by bespoke rules rather than by owner key, so no owner-key loop reaches
them and each has to be able to seed the sweep alone. The sweep already handles
all three -- the census seeds from them and `removeRepoFromWorkspaceSession`
clears them -- but nothing pinned it, and dropping that seeding turns all three
cases red.

The `activeWorkspaceKey` case uses the canonical `worktree:<id>` form, so it
also covers unwrapping the workspace key before the repo id is visible.

Refs #17776
2026-09-01 17:20:17 -07:00
Neil 87f3e907dd test(persistence): assert the sleeping-agent cleanup reached disk
The self-clearing check loaded a second store, but that constructor runs the
sweep itself. If the first flush had not persisted the cleanup, the second load
would have redone it in memory and the assertion would have passed without
meaning anything. Read the profile back and assert the map is empty there first.

Refs #17776
2026-09-01 17:20:17 -07:00
Neil 2f105b23d1 fix(persistence): sweep sleeping-agent-only residue and stop mis-seeding orphans
Review found three holes in the load-time sweep.

`sleepingAgentSessionsByPaneKey` and `terminalSurfaceTombstonesByPaneKey` are
pruned by the worktreeId they name, not by their own key, but
`pruneWorktreeStateForRepo` only collected owner keys from `worktreeMeta` and
`lastVisitedAtByWorktreeId`. An orphan whose only residue was a sleeping agent
therefore survived the sweep and re-seeded it on the next load, so the store
never self-cleared and every launch scheduled another save. Collect owner keys
from those records too, which fixes `removeProject` for the same shape.

`ownerKeyBelongsToRepo` is restored to its original body. Reordering its two
readings was not behavior-preserving as claimed: for a repo named `folder` or
`worktree`, checking the workspace-key reading first flips the result. The
census now uses `ownerKeyWorktreeIds`, which returns both readings, and seeds
only when neither names a live repo -- seeding one reading of a key whose
other reading is live would hand the removal pass a live row to delete.

Seed from `activeWorktreeId`, `activeWorkspaceKey` and
`activeWorktreeIdsOnShutdown`, which are pruned by bespoke rules and so were
reachable by no owner-key loop, and record why
`terminalTopologyRevisionByRepoId` stays excluded.

Refs #17776
2026-09-01 17:20:17 -07:00
Neil a2aea5d0b0 fix(persistence): sweep rows owned by deregistered repo ids at load
Deregistering a project stranded every row it owned. Each pruning path is
gated on the repo still being in `state.repos`, so once an id leaves the
catalogue its metadata, identity aliases, lineage and session rows became
unreachable forever -- and on a paired client they rendered as phantom
worktrees under an "Unknown" project.

Reconcile against the repo catalogue on load instead: any repo id that owns
rows but is absent from `state.repos` has its rows removed through the same
path `removeProject` uses. Host-independent and session-independent, because
an orphan has no owner that could object -- which is also why this reaches a
client's mirror of a remote host's session partition, something no local
removal can do.

Only a full `<repoId>::<path>` locator seeds the orphan set; bare keys can be
folder workspace ids or repo-keyed revisions, and guessing wrong there would
delete live state. `retiredWorktreeNamesByRepo` is deliberately untouched so a
re-added repo cannot reissue a name onto a cwd that still holds a prior
occupant's agent state.

Test fixtures that wrote worktree rows without registering their repo were
relying on orphans surviving a reload; they now register the repo they name.

Refs #17776
2026-09-01 17:20:17 -07:00
Neil 519af49a58 fix(dev): keep the shared Electron dist writable for the dev app
pn dev crashes on macOS in any worktree that adopted the shared Electron dist.
publishSharedElectronDist marks the cache entry read-only, which hardlink
sharing needs, but clonefile preserves mode -- so the dist lands 0555, the dev
runner copies it into out/electron-dev unchanged, and the first plutil -replace
on Info.plist fails with a permission error. The shipped zip has that file at
0644; on disk it is 0555, so the mode is ours, not upstream's.

copyPrivateTree now restores write permission. Its contract is a private tree
the caller goes on to patch, and its one production caller is the dev runner.

The test that should have caught this ran the wrapper with stdio: 'ignore', so a
hard crash presented as a bare 20s timeout. It now captures the wrapper's output
into the failure message, and waits long enough for the two synchronous swiftc
builds and a codesign --deep over ~280MB that precede the assertion.
2026-09-01 17:16:26 -07:00
Neilandkaluli123123 1e82f66e80 fix(agents): clear the unread completion marker when acknowledging agents (#17924)
Acknowledging is one action against two records, but only
clearTerminalPaneUnread cleared unreadAgentCompletionPanes. Acking from
the Activity page, the dashboard drawer or the popout bridge left the tab
dot, the ⌘J row and the floating-workspace dot lit with nothing left to
read; only the terminal-view auto-ack path cleared both.

Cleared inside the existing set so one ack is one commit, and only the
agent marker is touched — clearTerminalPaneUnread also drops
unreadTerminalPanes, which would silence a BEL the user never saw.

Refs #15445 (step 2 of that issue's fix; steps 1 and 3 remain open).

Co-authored-by: kaluli123123 <295758798+kaluli123123@users.noreply.github.com>
2026-09-01 17:15:15 -07:00
Neilandkaluli123123 80a52bb9b3 fix(git): recover commit ref badges on Git older than 2.43 (#17923)
GIT_HISTORY_COMMIT_FORMAT asked for decorations with %(decorate:…), which
Git 2.43 introduced. Older Git prints the placeholder verbatim and exits
zero, so nothing raised and every commit in the Source Control panel
silently lost its branch, remote and tag badges.

The record now also carries %D (Git 2.10) on its own line, selected by an
exact match against the unexpanded placeholder — a ref name can never
contain the \x1f that Git expands inside the echoed text. %n emits the %D
line on both sides of the boundary, so the message index is fixed and a
missed match degrades to no badges rather than a corrupted message.

The decoration separator is now bound to the field that produced the text
instead of sniffed from it. A lone decoration carries no separator, so the
old sniff split `refs/heads/feat,one` into two bogus refs.

Verified against real Git 2.38.1 and 2.49.1.

Co-authored-by: kaluli123123 <295758798+kaluli123123@users.noreply.github.com>
2026-09-01 17:14:57 -07:00
Neil c8937936eb refactor(mobile): pin the terminal WebView payload and split its widest slice
The payload is one concatenated string, so slice boundaries follow document
order rather than responsibility -- but join is associative, so cutting a slice
into consecutive slices is byte-identical by construction. Splits the widest
slice, which carried fit-scale, a DECSET scanner and the write queue together
with no room left under the line cap.

Adds a hash guard. The behavioral tests each execute one region of the payload
in a vm, so an edit to an uncovered region shipped silently; the composed output
is now pinned by sha256 and length.

Derives the source-file list from the composer's own imports instead of a second
hardcoded list a new slice had to be added to by hand -- the same silent
subject-loss shape already found twice elsewhere in this repo.
2026-09-01 16:38:22 -07:00
Neil 73fcdea23c fix(palette): recompute quick-action availability when runtime status changes
buildQuickActionContext reads runtimeStatusByEnvironmentId transitively through
getClientCreationActionPolicy, but the split dropped it from the memo deps. The
store replaces the Map identity on update, so the palette held availability from
a snapshot that never refreshed -- offering a browser action against a provider
that had gone away, or hiding one that had come back.

exhaustive-deps could not catch it: the read is behind a void statement, which
the rule does not see.
2026-09-01 16:38:06 -07:00
Neil 2c559fa96a test(child-process): make the import ratchet able to fail
never grows asserted offenders.length <= ALLOWLIST.length, but the two
membership assertions already force those equal, so it could not fail. The
comment claimed it caught a swap -- one file migrated off child_process, one
added -- which is exactly the case it let through.

Pins the true count and asserts both directions, so a swap fails and a pin left
stale-high after a migration also fails rather than banking ground twice. Gives
the console-visibility ratchet the same test: it had no count assertion at all
and the same gap.

Also anchors the owner-directory exemption with a trailing slash, so a future
src/shared/child-process-foo.ts is scanned rather than silently exempt.
2026-09-01 16:37:50 -07:00
Jinwoo-H c7335a8a1e Merge mr-p3-registry follow-up: value-based dispatch census
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 19:28:30 -04:00
Jinwoo-H 5746d6f1dc test(mobile): arm a native gesture before the hosted alert probe; split the task project client
native.alert is now gesture-gated, so the simulator journey taps the page
before posting the probe. The task project request client sat one line over
its max-lines cap; its project echo check moves to its own module.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 19:24:33 -04:00
Jinwoo-H 9adc88745a test(mobile): prove dispatch against the capability table, not quoted literals
The census this replaces collected every quoted literal under the shell tree and
asked whether the operation name was among them, so a comment or an unrelated
string satisfied it. It could not prove an operation was dispatched.

The new census resolves the table the broker actually calls: every registered
operation must reach a dispatch arm for its request mode, and each table must
carry arms for exactly the capabilities owning operations of that mode. Removing
either the file or the speech arm fails it, and so does adding an arm for a
capability that has no operation of that mode.

The remaining source census keeps the narrower step it can prove: the operation
name is matched at a dispatch position inside an arm, not merely quoted.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 19:23:41 -04:00
Jinwoo-H 3f8abd7715 Merge mr-p3-registry: registry censuses, grant table, dispatch tables, host-side branded ids
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 19:22:12 -04:00
Jinwoo-H ff8a13adaf refactor(mobile): brand the host-side workspace and repo identifiers
The authority now mints branded MobileWebHostWorkspaceId and MobileWebHostRepoId,
so passing a page handle where a resolved host identifier belongs is a compile
error. Reauthorization is the case that matters: assertHostWorkspaceBinding no
longer accepts the page handle as its expected host value.

Values the host itself reported go through two named boundary functions rather
than a bare cast, so every such site is greppable. No runtime check changed.

The page-side handles stay unbranded: branding MobileWebWorkspaceIdSchema alone
produced 527 type errors across the page and shell, and six more handle types
remain.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 19:18:11 -04:00
Jinwoo-H f838957c96 fix(mobile): keep /hybrid reachable under the E2E native baseline flag
The hosted simulator runner launches Metro with the native-baseline flag so
it can capture native screens, then opens orca:///hybrid. Since the
explicit-architecture change that same flag redirected /hybrid to /, so the
hosted WebView never mounted and the security journey timed out. Retiring
/hybrid is an architecture decision, not a baseline one.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 19:15:16 -04:00
Jinwoo-H d8127aa5b1 refactor(mobile-web): fold six copies of the workspace echo check into one
Six request clients each defined their own matchingWorkspace or matchingIdentity
over the same comparison, two of them with a different parameter shape. They now
call one shared implementation with its own test.

The echo census resolves the shared helper by name and its pinned per-operation
table is unchanged, so no operation lost a field.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 19:13:17 -04:00
Jinwoo-H 5df684b38c refactor(mobile): dispatch broker requests through tables instead of scans
The broker resolved a grant with a linear find over all 226 entries on every
request, including every terminal keystroke; it now reads an index keyed by
`capability.operation`.

The capability if-chain becomes two tables, one per request mode, holding the
same hand-written adapter functions. Each arm keeps its own sub-branching, its
gesture and payload checks, and its subscription lifecycle. Subscription arms
still refuse any operation but `subscribe`, and an unmatched capability still
fails closed.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 19:10:26 -04:00
Jinwoo-H db906b491d refactor(mobile): declare production grants through one operation-keyed table
Replaces three hand-written grant styles across 13 files with a single helper
keyed by MobileWebBridgeOperationName<capability>, so a typo or a retired
operation is a compile error instead of a grant the shell never matches. The
redundant operation union in the native-chat grants is gone; it restated the
registry.

Limits that referenced a contract constant still reference it. Every limit value
and the array order are unchanged: the resolved MOBILE_WEB_PRODUCTION_GRANTS is
byte-identical before and after.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 19:07:07 -04:00
Jinwoo-H a690f6e935 test(mobile-web): fence the bridge registry with kind, echo, and reauthorization censuses
Adds `kind: 'read' | 'mutation' | 'subscription'` to every registry entry and
three censuses that must keep passing across the declarative rewrites:

- schema pairing and dispatch coverage, so an operation cannot reach the shell
  through two contracts or leave without a dispatch arm
- an echo-field census pinning, per operation, the result fields its page client
  compares against the request
- a reauthorization census pinning every assertHost* site and forcing each
  mutation into a reauthorizing module or a recorded exemption

The registry becomes a record keyed by operation name; the capability enum now
derives from its keys instead of restating them.

`session.capabilities` is pinned as the one operation with two payload/result
contracts: its shell arm discriminates them with safeParse.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 19:01:42 -04:00
Neil 93a258c81d fix(worktrees): reclaim orphaned pr-* fork remotes (#17842)
* fix(worktrees): reclaim orphaned pr-* fork remotes

pr-* remotes Orca adds for fork-PR worktrees were only ever pruned by a
single worktree's own removal, and only when that removal had complete
provenance metadata, no branch pinning it, and actually ran through Orca.
Legacy metadata missing remoteCreated, "preserve branch on delete" pinning
the remote via branch.*.remote config after the worktree is gone, and
worktrees removed outside Orca entirely all left the remote behind
forever -- one real user accumulated ~50 leaked remotes this way.

Add a repo-scoped reconciliation sweep that inverts the existing cleanup
predicates over every pr-* remote instead of one removal, reusing
sameGitHubRemoteUrl/hasBranchConfigUsingRemote so no new safety logic is
introduced. It only touches a remote some worktree's persisted pushTarget
explicitly recorded Orca creating (remoteCreated: true) -- naming and URL
shape alone are not proof of provenance. Runs opportunistically alongside
existing single-target cleanup (including RuntimePreservedBranchCleanup's
force-delete path), rate-limited per repo, and fire-and-forget so it never
adds latency to the worktree-removal path a user is waiting on.

Fixes #17828

* test(worktrees): set a local git identity in the pr-remote fixture

CI runners have no global git identity, so `git commit` in the fixture
repos failed with "Author identity unknown" -- only passed locally because
dev machines have one. Set user.name/user.email (plus commit.gpgSign and
core.hooksPath, matching src/main/git/repo-remote-drift-real.test.ts) as
local repo config in both the main and cloned "fork" fixture repos, so the
test is independent of the runner's global config, signing setup, or hooks.
2026-09-01 15:59:03 -07:00
Jinwoo-H cf59537b69 test(mobile): pin gesture predicates per site and assert the tasks route is thin
Review found the gesture census could not tell a consuming gate from a
witnessing one, and the thin-route assertion could not fail. Also record
the one hook-order shift the parity digests carry.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 18:58:18 -04:00
Jinwoo-H f6f427af47 Merge mr-p4-docs: correct hybrid docs after the cleanup
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 18:49:36 -04:00
Jinwoo-H 3b2b1c0b36 docs(mobile): correct the hybrid WebView records against the tree
The audit record claimed a declarative registry prototype covered Tasks, Files,
and Session and proved metadata, grants, and typed calls can be generated. No
prototype existed: bridge-operation-registry.ts was an operation-name list with
a capability enum and a membership check, consumed only by bridge-contract.ts
and one test. Withdraw the claim, reopen the checklist item, and add a
2026-09-01 addendum recording the cleanup this branch landed, referenced by
merge subject.

Also correct three factual conflicts with the code:

- The rollback runbook told support to direct users to the retained native
  workspace route while its own Safety Invariants said the hybrid candidate has
  no such fallback. Scope that step to the native-default build, since
  isRetiredNativeWorkspaceRoute redirects every /h/... workspace route to
  /hybrid in a hybrid build.
- The architecture reference described the Android network fence as load
  blocking only, omitted that a sub-threshold WebView process restart now
  retires the capability broker, and did not say that the Android bridge
  accepts a message on the origin host derived from activeSessionId with the
  fragment as a secondary check. Name the build scripts that run each delivery
  step while there.
- The remaining-work tracker did not record that the native store suites now
  run in CI, or that hosted-mobile-webview-ssh.spec.ts is excluded from the
  ubuntu e2e lane because no hosted runner has a simulator and Docker at once.

mobile/README.md needed no change: every entrypoint in its e2e and native store
tables resolves to a real script.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 18:49:19 -04:00
Jinwoo-H 6c5677dc6a Merge remote-tracking branch 'origin/main' into mobile-rearch 2026-09-01 18:10:28 -04:00
Jinwoo-H dac547c799 Merge mr-p2-session: restore the split session route and RPC client
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 18:02:25 -04:00
Jinwoo-H b67d7bbceb refactor(mobile): restore main's RPC client split under the hybrid transport
Replace the 1,202-line rpc-client.ts closure with main's DirectRpcClient
decomposition (12 restored modules) and re-apply the branch's 33-hunk delta
onto it: the outbound backpressure queue and process memory budget, the
inbound frame queue with overflow/oversize close, JSON structure admission,
the extracted binary-frame handler, terminal multiplex routing with
sendTerminalBinaryFrame, accounts/files server-subscription teardown, and
protocol-only endpoint redaction.

Deletes rpc-client-contract.ts, rpc-client-request-state.ts and
rpc-client-subscribe-options.ts, which duplicated main's rpc-client.ts and
stream registry. Four helpers keep every restored module under the 300-line
cap without a suppression, so rpc-client.ts drops its max-lines override.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 17:59:08 -04:00
x 73960e0b0f refactor(mobile): restore the split session route over the hybrid delta
Rebuild app/h/[hostId]/session/[worktreeId].tsx as a 10-line route over
MobileSessionSurface + useMobileSessionController, restoring the 44 split
modules main deleted on this branch and re-applying the hybrid
operations-injection delta onto them.

Three behaviors main's own split had silently dropped are kept from the
branch instead: the accessory hold-to-repeat controller (#14219), the
relay-recovery input to classifyConnection, and the live MobileMarkdownReader
/ MobileDiffCommentLineRow components (main's split forked stale copies).

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 17:59:04 -04:00
Jinwoo-H e8099e85b6 Merge mr-p1-security follow-up: gradle ignore and README format
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

# Conflicts:
#	mobile/src/mobile-web/mobile-web-native-transport-source.test.ts
#	mobile/src/mobile-web/mobile-web-shell-root-route-source.test.ts
2026-09-01 17:56:07 -04:00
Jinwoo-H 1c755e5d56 chore(mobile): ignore local Gradle output and reformat the README
Running :orca-expo-mobile-web-shell:testDebugUnitTest locally leaves an
untracked build/ inside the package. The README tables were unformatted
and failed oxfmt --check.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 17:54:01 -04:00
Jinwoo-H 841870a4f6 fix(mobile-web): realign the Android source pins with the compiled Kotlin
7ae41c4f1f rewrote the bridge document-URL check onto java.net.URI and
hoisted activeSessionId into a local, which left two source pins asserting
text the Kotlin no longer contains. Both replacements pin the same
enforcement: explicit scheme/host/port on the bridge URL, and a
document-URL guard that resolves a non-null session before matching the
fragment.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 17:53:37 -04:00
Jinwoo Hong 7873f73d80 fix(daemon): keep attach cancellation behind client timeout (#17816) 2026-09-01 17:52:43 -04:00
Brennan BensonandMerge Sim 7314ada3fb fix(native-chat): ignore stale restored working status (#17995)
* fix(native-chat): ignore stale restored working status

* fix(native-chat): sample status freshness per epoch

* Revert "fix(native-chat): sample status freshness per epoch"

This reverts commit 8e49b1badd.

* test(native-chat): include hook status timestamps

* test(native-chat): include visibility hook timestamp

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-01 14:31:23 -07:00
Jinwoo-H da9af4a660 test(mobile-web): pin the Android bridge URL guard after the compile fix
The Kotlin compile fix replaced the android.net.Uri origin check in
MobileWebBridgeDocumentUrl with a JVM-testable host projection and
hoisted activeSessionId into a local; the source-parity assertions still
named the old spellings.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-01 17:29:18 -04:00