diff --git a/.github/scripts/e2e-with-window-manager.sh b/.github/scripts/e2e-with-window-manager.sh
new file mode 100644
index 00000000000..d431a039809
--- /dev/null
+++ b/.github/scripts/e2e-with-window-manager.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+set -euo pipefail
+openbox --sm-disable > /tmp/orca-e2e-window-manager.log 2>&1 &
+wm_pid=$!
+cleanup() {
+ kill "$wm_pid" 2>/dev/null || true
+ wait "$wm_pid" 2>/dev/null || true
+}
+trap cleanup EXIT
+ready=false
+for attempt in {1..100}; do
+ if xprop -root _NET_SUPPORTING_WM_CHECK 2>/dev/null | rg -q 'window id # 0x[1-9a-fA-F]'; then
+ ready=true
+ break
+ fi
+ if ! kill -0 "$wm_pid" 2>/dev/null; then
+ cat /tmp/orca-e2e-window-manager.log
+ exit 1
+ fi
+ sleep 0.1
+done
+if [ "$ready" != true ]; then
+ echo 'Window manager did not acquire the Xvfb root window' >&2
+ exit 1
+fi
+"$@"
diff --git a/.github/workflows/adhoc-mac-build.yml b/.github/workflows/adhoc-mac-build.yml
index d7dd6d5ffb6..3e17eee9b68 100644
--- a/.github/workflows/adhoc-mac-build.yml
+++ b/.github/workflows/adhoc-mac-build.yml
@@ -127,9 +127,12 @@ jobs:
esac
# Bare: a work-tree repo refuses to fetch over its own checked-out
# branch. tree:0 keeps the fetch to the commit graph — no trees, no
- # blobs — so this stays cheap next to the build it fronts.
+ # blobs — so this stays cheap next to the build it fronts. reftable
+ # because this repo has branches that differ only in casing, and the
+ # files backend cannot store both on a case-insensitive runner disk —
+ # it fails the entire fetch, not just the one ref.
scratch="$RUNNER_TEMP/vet-requested-ref"
- git init -q --bare "$scratch"
+ git init -q --bare --ref-format=reftable "$scratch"
git -C "$scratch" fetch -q --filter=tree:0 "$REPO_URL" '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'
# Branch first to keep actions/checkout's old tie-break: bare
# rev-parse would prefer the tag when a branch shares its name.
@@ -157,6 +160,9 @@ jobs:
- name: Checkout the requested ref
uses: actions/checkout@v6
+ env:
+ # Full-history checkout must also preserve case-twin branch and tag names.
+ GIT_DEFAULT_REF_FORMAT: reftable
with:
# Why an input at all rather than just github.ref: the whole point is to
# build code that has not landed, and the workflow definition itself
diff --git a/.github/workflows/cloud-verify.yml b/.github/workflows/cloud-verify.yml
index f0cc2df2bad..e2ba9407ac4 100644
--- a/.github/workflows/cloud-verify.yml
+++ b/.github/workflows/cloud-verify.yml
@@ -25,9 +25,10 @@ defaults:
working-directory: cloud
jobs:
+ # Public-repository hosted runners preserve Blacksmith allowance for macOS.
security:
name: Secret scan
- runs-on: blacksmith-2vcpu-ubuntu-2204
+ runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
@@ -53,7 +54,7 @@ jobs:
# Compiles the workspace. No Postgres service: nothing here reaches a
# database, and the service container costs ~13s of startup.
build:
- runs-on: blacksmith-4vcpu-ubuntu-2204
+ runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
@@ -73,7 +74,7 @@ jobs:
# package it needs through the relay pretest hook, so it does not depend on
# `pnpm build` having run.
test:
- runs-on: blacksmith-4vcpu-ubuntu-2204
+ runs-on: ubuntu-22.04
services:
postgres:
image: postgres:16-alpine
@@ -107,7 +108,7 @@ jobs:
# Fork pull requests reach this job, so it never configures a backend, never plans, and never
# holds a credential. Only the relay root ships here; foundation and apps stay private.
terraform:
- runs-on: blacksmith-2vcpu-ubuntu-2204
+ runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/dev-channel-win-build.yml b/.github/workflows/dev-channel-win-build.yml
index e16a50f1c3c..89fda2ebef9 100644
--- a/.github/workflows/dev-channel-win-build.yml
+++ b/.github/workflows/dev-channel-win-build.yml
@@ -149,9 +149,12 @@ jobs:
fi
# Reachability is the trust test: GitHub serves PR-only commits by SHA,
# so resolving the object is not proof a branch or tag of this repo
- # reaches it. Bare + tree:0 keeps this to the commit graph.
+ # reaches it. Bare + tree:0 keeps this to the commit graph; reftable
+ # because branches that differ only in casing cannot both be stored by
+ # the files backend on a case-insensitive runner disk, which fails the
+ # entire fetch rather than the one ref.
scratch="$RUNNER_TEMP/vet-requested-ref"
- git init -q --bare "$scratch"
+ git init -q --bare --ref-format=reftable "$scratch"
git -C "$scratch" fetch -q --filter=tree:0 "$REPO_URL" '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'
if ! git -C "$scratch" rev-parse --verify --quiet "$REQUESTED_SHA^{commit}" >/dev/null; then
echo "::error::Commit $REQUESTED_SHA is not in stablyai/orca."
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 3560d302a79..a94a7ea2ba5 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -27,6 +27,10 @@ on:
description: Ref to check out (defaults to the workflow ref)
required: false
type: string
+ test_files:
+ description: JSON array of specs to run; empty runs the full suite
+ required: false
+ type: string
schedule:
# Why: GitHub cron uses UTC; these slots map to 10am and 3pm
# America/Phoenix for the default-branch E2E run.
@@ -146,7 +150,7 @@ jobs:
# Native cache misses need the compiler, Electron needs Xvfb, and paired
# Quick Open needs ripgrep. Install them in one apt transaction per shard.
- name: Install native build and headless UI tools
- run: sudo apt-get update && sudo apt-get install -y build-essential fonts-noto-cjk python3 ripgrep xvfb zsh
+ run: sudo apt-get update && sudo apt-get install -y build-essential fonts-noto-cjk python3 ripgrep xvfb zsh openbox x11-utils
- uses: ./.github/actions/install-node-dependencies
with:
@@ -167,7 +171,7 @@ jobs:
# ORCA_E2E_FORWARD_APP_LOGS keeps startup failures visible when Electron
# launches but never creates a BrowserWindow.
- name: Run E2E tests (${{ matrix.shard_name }})
- run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 ORCA_E2E_WEB_CLIENT=1 ORCA_RELAY_PATH="$GITHUB_WORKSPACE/out/relay" pnpm run test:e2e --shard=${{ matrix.shard }}
+ run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 ORCA_E2E_WEB_CLIENT=1 ORCA_RELAY_PATH="$GITHUB_WORKSPACE/out/relay" pnpm run test:e2e --shard=${{ matrix.shard }}
# Why: Playwright retains traces/screenshots only on failure. Uploading
# them as an artifact makes post-mortem debugging on CI possible without
@@ -201,7 +205,7 @@ jobs:
# unbounded inventory fallback; the paired fixture exercises that real boundary.
# Why openssh-client: the Docker-SSH fixture shells out to ssh/ssh-keygen, and this
# lane now receives those specs from pr.yml's SSH source mapping.
- run: sudo apt-get update && sudo apt-get install -y build-essential fonts-noto-cjk openssh-client python3 ripgrep xvfb zsh
+ run: sudo apt-get update && sudo apt-get install -y build-essential fonts-noto-cjk openssh-client python3 ripgrep xvfb zsh openbox x11-utils
- uses: ./.github/actions/install-node-dependencies
with:
@@ -241,7 +245,7 @@ jobs:
if grep -l '@headful' "${TEST_FILES[@]}" >/dev/null; then
E2E_PROJECT_ARGS+=(--project=electron-headful)
fi
- xvfb-run --auto-servernum env "${E2E_ENV[@]}" \
+ xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env "${E2E_ENV[@]}" \
pnpm run test:e2e "${TEST_FILES[@]}" --workers=1 "${E2E_PROJECT_ARGS[@]}"
- name: Upload Playwright traces
@@ -278,7 +282,7 @@ jobs:
ref: ${{ inputs.ref || github.ref }}
- name: Install native build and headless UI tools
- run: sudo apt-get update && sudo apt-get install -y build-essential fonts-noto-cjk openssh-client python3 xvfb zsh
+ run: sudo apt-get update && sudo apt-get install -y build-essential fonts-noto-cjk openssh-client python3 ripgrep xvfb zsh openbox x11-utils
- uses: ./.github/actions/install-node-dependencies
with:
@@ -293,7 +297,7 @@ jobs:
# Why: this is the release-path proof that the deployed Linux relay keeps
# its PTY and explorer live across a real watcher SIGSEGV.
- name: Run Docker SSH watcher isolation E2E
- run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker-watcher-isolation
+ run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker-watcher-isolation
# Why: Playwright empties test-results/ when it starts, so each step here used to
# destroy the previous step's traces. Only the last lane's failure was ever
@@ -310,7 +314,7 @@ jobs:
# readiness across live SSH, headed paired, and headless serve topologies.
- name: Run Docker SSH terminal parking + startup readiness E2E
if: always()
- run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker-terminal-parking
+ run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker-terminal-parking
- name: Keep terminal-parking traces
if: always()
@@ -326,7 +330,7 @@ jobs:
# legible as an SSH-named failure.
- name: Run remaining Docker SSH E2E
if: always()
- run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker
+ run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker
- name: Keep remaining-ssh-docker traces
if: always()
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 4589a5a3ce1..6a058607ec3 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -93,12 +93,13 @@ jobs:
NATIVE_IME_SOURCE_CHANGED="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --native-ime-source)"
echo "native_ime_source_changed=$NATIVE_IME_SOURCE_CHANGED" >> "$GITHUB_OUTPUT"
echo "Native IME source changed: $NATIVE_IME_SOURCE_CHANGED"
- if [ "$TEST_FILES_JSON" != '[]' ]; then
+ SHOULD_RUN="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --reusable-workflow)"
+ if [ "$SHOULD_RUN" = true ]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "Changed E2E specs: $TEST_FILES_JSON"
else
echo "should_run=false" >> "$GITHUB_OUTPUT"
- echo "No changed E2E specs"
+ echo "No specs requiring the reusable E2E workflow"
fi
static_analysis:
diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml
index 6f888c3a512..001eee4e03c 100644
--- a/.github/workflows/release-cut.yml
+++ b/.github/workflows/release-cut.yml
@@ -858,16 +858,17 @@ jobs:
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y build-essential python3 xvfb
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version-file: package.json
-
- name: Setup pnpm
uses: pnpm/setup@v2
with:
install: false
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version-file: package.json
+ cache: pnpm
+
# Why: Linux terminal golden E2E uses the same native install path as
# release CI, which needs pnpm to bypass its non-executable gyp_main.py.
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
@@ -1074,16 +1075,17 @@ jobs:
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y build-essential python3 xvfb
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version-file: package.json
-
- name: Setup pnpm
uses: pnpm/setup@v2
with:
install: false
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version-file: package.json
+ cache: pnpm
+
# Why: keep the non-blocking evidence lane on the same Linux native
# install path as the blocking golden and release build jobs.
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
@@ -1716,6 +1718,7 @@ jobs:
with:
name: orca-windows-unsigned-${{ needs.cut.outputs.tag }}
path: dist/orca-windows-setup.exe
+ compression-level: 0
if-no-files-found: error
# Why: SignPath Foundation production certificates require manual review,
diff --git a/.github/workflows/release-ref-validation.yml b/.github/workflows/release-ref-validation.yml
new file mode 100644
index 00000000000..6995f9db174
--- /dev/null
+++ b/.github/workflows/release-ref-validation.yml
@@ -0,0 +1,38 @@
+name: Release ref validation
+
+on:
+ pull_request:
+ paths:
+ - '.github/workflows/adhoc-mac-build.yml'
+ - '.github/workflows/dev-channel-win-build.yml'
+ - '.github/workflows/release-ref-validation.yml'
+ - 'config/scripts/workflow-ref-reachability.test.mjs'
+ - 'config/scripts/workflow-ref-mirror-case-safety.test.mjs'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: release-ref-validation-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ validate:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [macos-15, windows-2022]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+ - uses: ./.github/actions/install-node-dependencies
+ - name: Verify case-twin refs and release trust boundary
+ run: >-
+ pnpm exec vitest run --config config/vitest.config.ts
+ config/scripts/workflow-ref-reachability.test.mjs
+ config/scripts/workflow-ref-mirror-case-safety.test.mjs
+ config/scripts/dev-channel-windows-workflow-contract.test.mjs
diff --git a/.github/workflows/skill-update-roundtrip.yml b/.github/workflows/skill-update-roundtrip.yml
index 239f1b2f27c..96de1101275 100644
--- a/.github/workflows/skill-update-roundtrip.yml
+++ b/.github/workflows/skill-update-roundtrip.yml
@@ -45,7 +45,9 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
+ # Historical skill snapshots need tags, but only their blobs are read.
fetch-depth: 0
+ filter: blob:none
persist-credentials: false
- uses: actions/setup-node@v6
with:
diff --git a/.github/workflows/terminal-ime-e2e.yml b/.github/workflows/terminal-ime-e2e.yml
index b9957b2daa9..1ab905d8783 100644
--- a/.github/workflows/terminal-ime-e2e.yml
+++ b/.github/workflows/terminal-ime-e2e.yml
@@ -38,23 +38,9 @@ jobs:
xfwm4
xvfb
- - name: Setup Node.js
- uses: actions/setup-node@v6
+ - uses: ./.github/actions/install-node-dependencies
with:
- node-version-file: package.json
-
- - name: Setup pnpm
- uses: pnpm/setup@v2
- with:
- install: false
-
- - name: Use external node-gyp to avoid pnpm bundled copy
- run: |
- npm install -g node-gyp@11.5.0
- echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
-
- - name: Install dependencies
- run: pnpm install --frozen-lockfile
+ native-runtime: electron
- name: Build Electron app for E2E
run: pnpm exec electron-vite build --mode e2e
diff --git a/.github/workflows/terminal-perf.yml b/.github/workflows/terminal-perf.yml
index 38d0a25bbb7..72a0fec8992 100644
--- a/.github/workflows/terminal-perf.yml
+++ b/.github/workflows/terminal-perf.yml
@@ -67,16 +67,17 @@ jobs:
- name: Install native build tools and xvfb
run: sudo apt-get update && sudo apt-get install -y build-essential python3 xvfb zsh
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version-file: package.json
-
- name: Setup pnpm
uses: pnpm/setup@v2
with:
install: false
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version-file: package.json
+ cache: pnpm
+
# Why: this scheduled/manual workflow uses the same native install path as
# PR and E2E CI, which needs pnpm to bypass its bundled gyp_main.py.
- name: Use external node-gyp to avoid pnpm's bundled copy
diff --git a/.github/workflows/windows-signing-rehearsal.yml b/.github/workflows/windows-signing-rehearsal.yml
index 54b751908dc..6fc6fab7193 100644
--- a/.github/workflows/windows-signing-rehearsal.yml
+++ b/.github/workflows/windows-signing-rehearsal.yml
@@ -215,6 +215,7 @@ jobs:
with:
name: orca-windows-installer-unsigned-${{ github.run_id }}
path: dist/orca-windows-setup.exe
+ compression-level: 0
if-no-files-found: error
- name: Submit Windows installer signing request
diff --git a/.gitignore b/.gitignore
index 7c6277bda3a..6722fc5ae54 100644
--- a/.gitignore
+++ b/.gitignore
@@ -111,6 +111,7 @@ docs/**
!docs/reference/orcad-operations.md
!docs/reference/relay-grace-time-reconfiguration.md
!docs/reference/windows-cmd-shim-resolution.md
+!docs/reference/windows-daemon-host-relocation.md
!docs/reference/windows-edr-posture.md
!docs/reference/windows-process-enumeration.md
!docs/reference/wsl-runner-verification.md
diff --git a/AGENTS.md b/AGENTS.md
index f8a875440b0..b0947da0c2f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,6 +4,12 @@ All UI work — layout, color, typography, spacing, component selection, UX beha
## Electron UI Validation
+Always run tests and agent-launched apps in the background with `ORCA_BACKGROUND_LAUNCH=1`.
+Never steal monitor focus or reveal test windows: no `show()`, `showInactive()`, `bringToFront()`,
+`app.focus()`, or OS activation. Use CDP screenshots of hidden renderers. Keep native-focus and
+visible-window tests paused on the user's desktop; run them on an isolated display or CI.
+Rebuild modified launch-policy code before running an app; stale build wrappers are not safe.
+
Use the `$electron` skill and Playwright CDP for rendered Orca UI checks. Do not use computer-use for Orca UI validation.
# Style
@@ -49,6 +55,7 @@ Orca targets macOS, Linux, and Windows. Keep all platform-dependent behavior beh
- **Windows setup scripts**: the setup/issue-command runner is a `.cmd` batch file unless the script starts with a `#!` line — never derive that from the user's terminal-shell preference, and never launch a `.cmd` runner with a bare `cmd.exe /c` from a Git Bash pane (MSYS rewrites the `/c`). See [`docs/reference/windows-setup-shell.md`](./docs/reference/windows-setup-shell.md).
- **Windows child processes**: start them through `runProcess`/`spawnProcess` in `src/shared/child-process/` — never `child_process` directly. It pins `windowsHide`, refuses `shell: true`, and encodes `.cmd`/`.bat` arguments so neither `CommandLineToArgvW` nor `cmd.exe` mangles them. A ratchet test fails on any new direct import. Recognised npm/pnpm `.cmd` shims are resolved to their real target so the spawn skips `cmd.exe` entirely; see [`docs/reference/windows-cmd-shim-resolution.md`](./docs/reference/windows-cmd-shim-resolution.md) before adding a shim shape or debugging one.
- **Windows process enumeration**: read the table through `src/main/windows/windows-process-table.ts`, never by forking `powershell.exe`. See [`docs/reference/windows-process-enumeration.md`](./docs/reference/windows-process-enumeration.md).
+- **Windows daemon-host relocation**: the terminal daemon runs from a copy of the app runtime under `%LOCALAPPDATA%`, which is what survives an auto-update. Before touching that copy, its exe name, or the NSIS uninstall macro, read [`docs/reference/windows-daemon-host-relocation.md`](./docs/reference/windows-daemon-host-relocation.md).
- **Windows EDR signal**: don't add `-ExecutionPolicy Bypass`, `-EncodedCommand`, `cmd.exe /c` with escaped free text, per-operation interpreter spawning, or runtime `Add-Type` compilation without reading [`docs/reference/windows-edr-posture.md`](./docs/reference/windows-edr-posture.md) first — behavioural EDR scores each of those, and being signed does not clear them.
- **WSL commands**: build argv with `buildWslExecArgs` (always `--exec` — under `--`, `wsl.exe` expands `$name` in every argument and silently rewrites the script), and fence anything whose stdout you parse with `buildWslCapturedLoginShellCommand`, because the interactive login shell prints the distro banner to stdout. See [`docs/reference/wsl-command-execution.md`](./docs/reference/wsl-command-execution.md).
- **Linux native modules**: keep the glibc floor at Ubuntu 20.04 / glibc 2.31. A module compiled from source on a newer runner can reference symbol versions absent on the floor and crash the app on startup. See [`docs/reference/linux-glibc-compatibility.md`](./docs/reference/linux-glibc-compatibility.md); packaging fails if a bundled native binary needs newer glibc.
diff --git a/config/nsis/orca-installer-hooks.nsh b/config/nsis/orca-installer-hooks.nsh
index ca80c99fc6d..d89439073ab 100644
--- a/config/nsis/orca-installer-hooks.nsh
+++ b/config/nsis/orca-installer-hooks.nsh
@@ -49,22 +49,48 @@
; ---------------------------------------------------------------------------
; Clean up the relocated terminal daemon on a REAL uninstall.
;
-; Why: the daemon host is deliberately copied to a distinct image name
-; (orca-terminal-daemon.exe) under %LOCALAPPDATA%\Orca\daemon-host so that app
-; UPDATES cannot kill it — that relocation is what keeps terminals alive across
-; updates. The same design means a normal uninstall's process sweep and file
-; removal both miss it, leaving an orphaned daemon plus its runtime copy behind.
+; Why: the daemon host is deliberately copied OUT of the install dir into
+; %LOCALAPPDATA%\Orca\daemon-host so that app UPDATES cannot kill it —
+; electron-builder's kill sweep selects processes whose image path is under
+; $INSTDIR, and that relocation is what keeps terminals alive across updates.
+; The same design means a normal uninstall's process sweep and file removal both
+; miss it, leaving an orphaned daemon plus its runtime copy behind.
;
; The ${isUpdated} guard is essential: electron-builder runs this uninstaller as
; part of uninstallOldVersion on EVERY update, and killing the daemon there would
; defeat the whole feature. Only clean up on a genuine uninstall.
;
-; The image name and the LOCALAPPDATA folder name must stay in sync with
-; DAEMON_HOST_EXE_NAME and LOCAL_HOST_ROOT_NAME in
-; src/main/daemon/daemon-host-relocation.ts.
+; The LOCALAPPDATA folder name must stay in sync with LOCAL_HOST_ROOT_NAME in
+; src/main/daemon/daemon-host-relocation.ts. See
+; docs/reference/windows-daemon-host-relocation.md.
!macro customUnInstall
${ifNot} ${isUpdated}
- nsExec::Exec 'taskkill /F /IM orca-terminal-daemon.exe'
+ Push $0
+ Push $1
+ Push $2
+ ; The host exe is a verbatim copy of the app exe, so the app's own image name
+ ; reaches it; the second name covers hosts left by builds that renamed the copy.
+ ; Filtered to the current user like upstream's per-user KILL_PROCESS, so an
+ ; elevated machine-wide uninstall cannot reach another logged-on user's session.
+ ; NSIS expands USERNAME itself: routing through cmd.exe only to get %USERNAME%
+ ; would add two interpreter spawns to the uninstall path for nothing.
+ ReadEnvStr $1 USERNAME
+ ${if} $1 == ""
+ ; Measured: taskkill rejects an empty filter value outright ("The search filter
+ ; cannot be recognized") and kills nothing, so with no USERNAME to scope by,
+ ; kill unfiltered rather than not at all. USERNAME is set in every session an
+ ; uninstaller runs in, so this is a backstop, not the expected path.
+ StrCpy $2 ""
+ ${else}
+ StrCpy $2 '/FI "USERNAME eq $1"'
+ ${endIf}
+ nsExec::Exec 'taskkill /F /IM "${APP_EXECUTABLE_FILENAME}" $2'
+ Pop $0
+ nsExec::Exec 'taskkill /F /IM "orca-terminal-daemon.exe" $2'
+ Pop $0
+ Pop $2
+ Pop $1
+ Pop $0
; Give the OS a moment to release the image lock before removing the tree.
Sleep 500
RMDir /r "$LOCALAPPDATA\Orca\daemon-host"
diff --git a/config/scripts/benchmark-browser-tunnel-framing.mjs b/config/scripts/benchmark-browser-tunnel-framing.mjs
new file mode 100644
index 00000000000..e91fd0887f6
--- /dev/null
+++ b/config/scripts/benchmark-browser-tunnel-framing.mjs
@@ -0,0 +1,110 @@
+import assert from 'node:assert/strict'
+import { execFileSync } from 'node:child_process'
+import { readFileSync } from 'node:fs'
+import { stripTypeScriptTypes } from 'node:module'
+import { performance } from 'node:perf_hooks'
+
+// Run from the worktree root: node config/scripts/benchmark-browser-tunnel-framing.mjs [base-ref]
+const path = 'src/shared/browser-network-tunnel-stream-framing.ts'
+const baselineRef = process.argv[2] ?? 'HEAD'
+const beforeSource = execFileSync('git', ['show', `${baselineRef}:${path}`], {
+ encoding: 'utf8'
+})
+const afterSource = readFileSync(path, 'utf8')
+const load = (source) =>
+ import(
+ `data:text/javascript;base64,${Buffer.from(
+ stripTypeScriptTypes(source, { mode: 'transform' })
+ ).toString('base64')}`
+ )
+const before = await load(beforeSource)
+const after = await load(afterSource)
+
+function measure(module, chunks, payload, repetitions) {
+ let frameCount = 0
+ let lastFrame
+ const onFrame = (frame) => {
+ frameCount++
+ lastFrame = frame
+ }
+ const onError = (error) => {
+ throw error
+ }
+ const run = () => {
+ const decoder = new module.BrowserNetworkTunnelStreamFrameDecoder(onFrame, onError)
+ for (const chunk of chunks) {
+ decoder.feed(chunk)
+ }
+ }
+ run()
+ assert.deepEqual(lastFrame, payload)
+ const samples = []
+ for (let sample = 0; sample < 5; sample++) {
+ const start = performance.now()
+ for (let iteration = 0; iteration < repetitions; iteration++) {
+ run()
+ }
+ samples.push((performance.now() - start) / repetitions)
+ }
+ assert.equal(frameCount, 1 + 5 * repetitions)
+ return samples.sort((a, b) => a - b)[2]
+}
+
+function countCopies(module, chunks) {
+ const originalSet = Uint8Array.prototype.set
+ const originalSlice = Uint8Array.prototype.slice
+ let copied = 0
+ Uint8Array.prototype.set = function (source, offset) {
+ copied += source.length
+ return originalSet.call(this, source, offset)
+ }
+ Uint8Array.prototype.slice = function (...args) {
+ const result = originalSlice.apply(this, args)
+ copied += result.length
+ return result
+ }
+ try {
+ const decoder = new module.BrowserNetworkTunnelStreamFrameDecoder(
+ () => {},
+ (error) => {
+ throw error
+ }
+ )
+ for (const chunk of chunks) {
+ decoder.feed(chunk)
+ }
+ } finally {
+ Uint8Array.prototype.set = originalSet
+ Uint8Array.prototype.slice = originalSlice
+ }
+ return copied
+}
+
+const rows = []
+for (const [payloadBytes, chunkBytes, repetitions] of [
+ [1, 5, 10000],
+ [64 * 1024, 65540, 1000],
+ [64 * 1024, 4096, 100],
+ [64 * 1024, 256, 25],
+ [64 * 1024, 16, 5],
+ [64 * 1024, 1, 1]
+]) {
+ const payload = Uint8Array.from({ length: payloadBytes }, (_, index) => index % 251)
+ const encoded = before.encodeBrowserNetworkTunnelStreamFrame(payload)
+ const chunks = []
+ for (let offset = 0; offset < encoded.length; offset += chunkBytes) {
+ chunks.push(encoded.subarray(offset, offset + chunkBytes))
+ }
+ const beforeMs = measure(before, chunks, payload, repetitions)
+ const afterMs = measure(after, chunks, payload, repetitions)
+ rows.push({
+ payloadBytes,
+ chunkBytes,
+ beforeMs: +beforeMs.toFixed(6),
+ afterMs: +afterMs.toFixed(6),
+ speedup: +(beforeMs / afterMs).toFixed(2),
+ beforeCopiedBytes: countCopies(before, chunks),
+ afterCopiedBytes: countCopies(after, chunks)
+ })
+}
+console.log(JSON.stringify({ node: process.version, baselineRef, rows }, null, 2))
diff --git a/config/scripts/benchmark-cli-error-imports.mjs b/config/scripts/benchmark-cli-error-imports.mjs
new file mode 100644
index 00000000000..a4648f84aec
--- /dev/null
+++ b/config/scripts/benchmark-cli-error-imports.mjs
@@ -0,0 +1,121 @@
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import { existsSync, realpathSync } from 'node:fs'
+import { delimiter, join, resolve } from 'node:path'
+
+// Emit each revision with tsc -p config/tsconfig.cli.json --outDir
--composite false --incremental false.
+// Run: node config/scripts/benchmark-cli-error-imports.mjs
+const [beforeDir, afterDir] = process.argv.slice(2)
+assert.ok(beforeDir && afterDir, 'Pass distinct before and after TypeScript output directories.')
+assert.notEqual(
+ realpathSync(beforeDir),
+ realpathSync(afterDir),
+ 'Do not compare a build to itself.'
+)
+const entries = {
+ before: join(resolve(beforeDir), 'cli', 'index.js'),
+ after: join(resolve(afterDir), 'cli', 'index.js')
+}
+for (const entry of Object.values(entries)) {
+ assert.ok(existsSync(entry), `Missing emitted CLI: ${entry}`)
+}
+
+const { runProcessSync } = createRequire(import.meta.url)(
+ join(resolve(afterDir), 'shared', 'child-process', 'run-process.js')
+)
+
+const child = String.raw`
+ const { performance } = require('node:perf_hooks')
+ const { writeSync } = require('node:fs')
+ const { createHash } = require('node:crypto')
+ const { basename } = require('node:path')
+ let stdout = '', stderr = ''
+ process.stdout.write = (text) => { stdout += text; return true }
+ process.stderr.write = (text) => { stderr += text; return true }
+ const started = performance.now()
+ const cli = require(process.argv[1])
+ const importMs = performance.now() - started
+ cli.main(JSON.parse(process.argv[2])).then(() => {
+ const totalMs = performance.now() - started
+ const modules = Object.keys(require.cache)
+ writeSync(1, JSON.stringify({
+ importMs, totalMs, modules: modules.length,
+ featureFormatters: modules.filter((file) => ['browser', 'terminal', 'project', 'automation', 'workspace', 'computer'].some((name) => basename(file) === name + '-format.js')),
+ stdout: createHash('sha256').update(stdout).digest('hex'),
+ stderr: createHash('sha256').update(stderr).digest('hex'),
+ exitCode: process.exitCode || 0
+ }))
+ process.exitCode = 0
+ }).catch((error) => { writeSync(2, String(error)); process.exitCode = 1 })
+`
+const cases = [
+ ['--help'],
+ ['help', 'terminal', 'read'],
+ ['does-not-exist'],
+ ['computer', 'click', '--does-not-exist'],
+ ['does-not-exist', '--json']
+]
+const median = (values) => [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)]
+const summarize = (samples) => ({
+ importMs: median(samples.map((sample) => sample.importMs)),
+ totalMs: median(samples.map((sample) => sample.totalMs)),
+ modules: samples[0].modules
+})
+const rows = []
+for (const args of cases) {
+ const samples = { before: [], after: [] }
+ let expected
+ for (let run = 0; run < 22; run++) {
+ for (const variant of run % 2 ? ['after', 'before'] : ['before', 'after']) {
+ const result = runProcessSync({
+ program: process.execPath,
+ args: ['-e', child, entries[variant], JSON.stringify(args)],
+ timeoutMs: 30_000,
+ env: {
+ ...process.env,
+ NODE_PATH: [resolve('node_modules'), process.env.NODE_PATH]
+ .filter(Boolean)
+ .join(delimiter)
+ }
+ })
+ assert.equal(result.timedOut, false, 'CLI child timed out.')
+ assert.equal(result.code, 0, result.stderr)
+ const sample = JSON.parse(result.stdout)
+ const output = { stdout: sample.stdout, stderr: sample.stderr, exitCode: sample.exitCode }
+ expected ??= output
+ assert.deepEqual(output, expected, `${variant} output changed for ${args.join(' ')}`)
+ if (variant === 'after') {
+ assert.deepEqual(
+ sample.featureFormatters,
+ [],
+ 'Help and syntax errors must skip feature formatters.'
+ )
+ }
+ if (run >= 2) {
+ samples[variant].push(sample)
+ }
+ }
+ }
+ assert.ok(samples.after[0].modules < samples.before[0].modules, 'Expected fewer loaded modules.')
+ rows.push({
+ args,
+ before: summarize(samples.before),
+ after: summarize(samples.after),
+ output: expected,
+ samples
+ })
+}
+console.log(
+ JSON.stringify(
+ {
+ node: process.version,
+ platform: process.platform,
+ measurement:
+ 'Fresh-process import + main; excludes process creation; warmed filesystem; 2 warmups and 20 samples per variant, alternating order.',
+ entries,
+ rows
+ },
+ null,
+ 2
+ )
+)
diff --git a/config/scripts/benchmark-cli-response-framing.mjs b/config/scripts/benchmark-cli-response-framing.mjs
new file mode 100644
index 00000000000..40aab8d08f7
--- /dev/null
+++ b/config/scripts/benchmark-cli-response-framing.mjs
@@ -0,0 +1,128 @@
+import assert from 'node:assert/strict'
+import { execFileSync } from 'node:child_process'
+import { EventEmitter } from 'node:events'
+import { readFileSync } from 'node:fs'
+import Module from 'node:module'
+import { dirname, resolve } from 'node:path'
+import { performance } from 'node:perf_hooks'
+import { build } from 'esbuild'
+
+// Run from the worktree root: node config/scripts/benchmark-cli-response-framing.mjs
+const sourcePath = 'src/cli/runtime/transport.ts'
+const baselineRef = process.argv[2]
+assert.ok(baselineRef, 'Pass the pre-change transport revision as base-ref.')
+const beforeSource = execFileSync('git', ['show', `${baselineRef}:${sourcePath}`], {
+ encoding: 'utf8'
+})
+let chunks = []
+
+async function loadTransport(source) {
+ const built = await build({
+ stdin: { contents: source, loader: 'ts', resolveDir: dirname(resolve(sourcePath)) },
+ bundle: true,
+ platform: 'node',
+ format: 'cjs',
+ write: false,
+ logLevel: 'silent'
+ })
+ const module = new Module(resolve(sourcePath))
+ const originalRequire = module.require.bind(module)
+ module.require = (name) => {
+ if (name === 'node:crypto') {
+ return { randomUUID: () => 'benchmark-request' }
+ }
+ if (name !== 'node:net') {
+ return originalRequire(name)
+ }
+ return {
+ createConnection() {
+ const socket = new EventEmitter()
+ socket.setEncoding = () => {}
+ socket.end = () => {}
+ socket.destroy = () => {}
+ socket.write = () => {
+ for (const chunk of chunks) {
+ socket.emit('data', chunk)
+ }
+ }
+ queueMicrotask(() => socket.emit('connect'))
+ return socket
+ }
+ }
+ }
+ module._compile(built.outputFiles[0].text, resolve(sourcePath))
+ return module.exports.sendRequest
+}
+
+const before = await loadTransport(beforeSource)
+const after = await loadTransport(readFileSync(sourcePath, 'utf8'))
+const metadata = {
+ runtimeId: 'benchmark-runtime',
+ authToken: 'benchmark-token',
+ transports: [{ kind: 'unix', endpoint: 'injected-socket' }]
+}
+const run = (sendRequest) => sendRequest(metadata, 'terminal.read', {}, 30000)
+
+async function measure(sendRequest, payloadBytes, repetitions) {
+ const warmup = await run(sendRequest)
+ assert.equal(warmup.result.data.length, payloadBytes)
+ const samples = []
+ for (let sample = 0; sample < 5; sample++) {
+ const start = performance.now()
+ for (let iteration = 0; iteration < repetitions; iteration++) {
+ await run(sendRequest)
+ }
+ samples.push((performance.now() - start) / repetitions)
+ }
+ return samples.sort((a, b) => a - b)[2]
+}
+
+async function searchedCharacters(sendRequest) {
+ const original = String.prototype.indexOf
+ let searched = 0
+ String.prototype.indexOf = function (needle, position) {
+ if (needle === '\n') {
+ searched += this.length - (position ?? 0)
+ }
+ return original.call(this, needle, position)
+ }
+ try {
+ await run(sendRequest)
+ } finally {
+ String.prototype.indexOf = original
+ }
+ return searched
+}
+
+const rows = []
+for (const [payloadBytes, chunkChars, repetitions] of [
+ [32, 65536, 1000],
+ [1024 * 1024, 2 * 1024 * 1024, 20],
+ [1024 * 1024, 65536, 10],
+ [1024 * 1024, 4096, 5],
+ [4 * 1024 * 1024, 4096, 2],
+ [4 * 1024 * 1024, 256, 1]
+]) {
+ const line = `${JSON.stringify({
+ id: 'benchmark-request',
+ ok: true,
+ result: { data: 'x'.repeat(payloadBytes) },
+ _meta: { runtimeId: 'benchmark-runtime' }
+ })}\n`
+ chunks = []
+ for (let offset = 0; offset < line.length; offset += chunkChars) {
+ chunks.push(line.slice(offset, offset + chunkChars))
+ }
+ const beforeMs = await measure(before, payloadBytes, repetitions)
+ const afterMs = await measure(after, payloadBytes, repetitions)
+ rows.push({
+ payloadBytes,
+ chunkChars,
+ beforeMs: +beforeMs.toFixed(6),
+ afterMs: +afterMs.toFixed(6),
+ speedup: +(beforeMs / afterMs).toFixed(2),
+ beforeSearchedCharacters: await searchedCharacters(before),
+ afterSearchedCharacters: await searchedCharacters(after)
+ })
+}
+console.log(JSON.stringify({ node: process.version, baselineRef, rows }, null, 2))
diff --git a/config/scripts/benchmark-explorer-dotfile-filter.mjs b/config/scripts/benchmark-explorer-dotfile-filter.mjs
new file mode 100644
index 00000000000..e66a0ceb5af
--- /dev/null
+++ b/config/scripts/benchmark-explorer-dotfile-filter.mjs
@@ -0,0 +1,165 @@
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import Module from 'node:module'
+import { resolve } from 'node:path'
+import { performance } from 'node:perf_hooks'
+import { build } from 'esbuild'
+
+// Pass the pre-change file-explorer-entries.ts snapshot as the only argument.
+const baselinePath = process.argv[2]
+assert.ok(baselinePath, 'Pass a pre-change file-explorer-entries.ts snapshot.')
+const entry = 'src/renderer/src/components/right-sidebar/file-explorer-entries.ts'
+const baseline = readFileSync(baselinePath, 'utf8')
+assert.notEqual(baseline, readFileSync(entry, 'utf8'), 'Do not compare the source to itself.')
+
+async function load(useBaseline) {
+ const result = await build({
+ stdin: {
+ contents: `export { isDotfileRelativePath } from './${entry}';
+export { createNameFilteredFileExplorerProjection } from './src/renderer/src/components/right-sidebar/file-explorer-name-filter-projection.ts';`,
+ resolveDir: process.cwd(),
+ loader: 'ts'
+ },
+ bundle: true,
+ platform: 'node',
+ format: 'cjs',
+ write: false,
+ logLevel: 'silent',
+ alias: { '@': resolve('src/renderer/src') },
+ plugins: useBaseline
+ ? [
+ {
+ name: 'baseline-dotfile-predicate',
+ setup(builder) {
+ builder.onLoad({ filter: /file-explorer-entries\.ts$/ }, () => ({
+ contents: baseline,
+ loader: 'ts'
+ }))
+ }
+ }
+ ]
+ : []
+ })
+ const module = new Module(resolve('dotfile-benchmark.cjs'))
+ module.paths = Module._nodeModulePaths(process.cwd())
+ module._compile(result.outputFiles[0].text, module.id)
+ return module.exports
+}
+
+const versions = [await load(true), await load(false)]
+let parityCases = 0
+function check(path, depth) {
+ assert.equal(
+ versions[0].isDotfileRelativePath(path),
+ versions[1].isDotfileRelativePath(path),
+ path
+ )
+ parityCases++
+ if (depth > 0) {
+ for (const character of ['.', '/', '\\', 'a', '\n']) {
+ check(path + character, depth - 1)
+ }
+ }
+}
+check('', 8)
+
+function measure(functions, iterations = 1) {
+ let sink = 0
+ const run = (fn) => {
+ for (let i = 0; i < iterations; i++) {
+ sink += Number(fn())
+ }
+ }
+ for (const fn of functions) {
+ for (let warmup = 0; warmup < 3; warmup++) {
+ run(fn)
+ }
+ }
+ const samples = [[], []]
+ for (let round = 0; round < 11; round++) {
+ for (const variant of round % 2 ? [1, 0] : [0, 1]) {
+ const start = performance.now()
+ run(functions[variant])
+ samples[variant].push(performance.now() - start)
+ }
+ }
+ return {
+ beforeMs: samples[0].sort((a, b) => a - b)[5],
+ afterMs: samples[1].sort((a, b) => a - b)[5],
+ iterations,
+ sink
+ }
+}
+
+const predicates = []
+for (const path of [
+ 'a',
+ '.env',
+ 'packages/pkg/src/file.tsx',
+ `a${'.'.repeat(254)}`,
+ `${'/'.repeat(4096)}.`,
+ `${'../'.repeat(1000)}file.ts`,
+ '😀/.你好',
+ '\n/.\n'
+]) {
+ check(path, 0)
+ predicates.push({
+ pathLength: path.length,
+ prefix: path.slice(0, 40),
+ ...measure(
+ versions.map((version) => () => version.isDotfileRelativePath(path)),
+ 10_000
+ )
+ })
+}
+
+const projections = []
+for (const count of [1000, 10_000, 100_000]) {
+ for (const query of ['nonmatching-needle', 'file-42']) {
+ const args = {
+ ignoredSet: new Set(['unrelated']),
+ nameFilter: {
+ query,
+ relativePaths: Array.from(
+ { length: count },
+ (_, i) => `packages/package-${i % 50}/src/components/section-${i % 10}/file-${i}.tsx`
+ )
+ },
+ showDotfiles: false,
+ showGitIgnoredFiles: false,
+ worktreePath: '/workspace'
+ }
+ const functions = versions.map(
+ (version) => () => version.createNameFilteredFileExplorerProjection(args)
+ )
+ const rows = functions.map((fn) => {
+ const projection = fn()
+ return Array.from({ length: projection.getVisibleCount() }, (_, i) =>
+ projection.getRowAtIndex(i)
+ )
+ })
+ assert.deepEqual(rows[0], rows[1])
+ projections.push({
+ count,
+ query,
+ visibleRows: rows[0].length,
+ ...measure(functions.map((fn) => () => fn().getVisibleCount()))
+ })
+ }
+}
+console.log(
+ JSON.stringify(
+ {
+ node: process.version,
+ platform: process.platform,
+ baselinePath: resolve(baselinePath),
+ parityCases,
+ samples: 11,
+ warmups: 3,
+ predicates,
+ projections
+ },
+ null,
+ 2
+ )
+)
diff --git a/config/scripts/benchmark-sentinel-retention.mjs b/config/scripts/benchmark-sentinel-retention.mjs
new file mode 100644
index 00000000000..93564eeac01
--- /dev/null
+++ b/config/scripts/benchmark-sentinel-retention.mjs
@@ -0,0 +1,72 @@
+import { strict as assert } from 'node:assert'
+import { EventEmitter } from 'node:events'
+import { mkdtemp, rm } from 'node:fs/promises'
+import { createRequire } from 'node:module'
+import { tmpdir } from 'node:os'
+import { join, resolve } from 'node:path'
+import { build } from 'esbuild'
+
+if (!global.gc) {
+ throw new Error('Run with node --expose-gc')
+}
+const root = resolve(import.meta.dirname, '../..')
+const directory = await mkdtemp(join(tmpdir(), 'orca-sentinel-retention-'))
+const output = join(directory, 'sentinel.cjs')
+try {
+ await build({
+ stdin: {
+ contents: `export {waitForSentinel} from './src/main/ssh/ssh-relay-deploy-helpers';
+export {RELAY_SENTINEL} from './src/main/ssh/relay-protocol';`,
+ resolveDir: root,
+ loader: 'ts'
+ },
+ bundle: true,
+ platform: 'node',
+ format: 'cjs',
+ packages: 'external',
+ banner: {
+ js: `var require = require('node:module').createRequire(${JSON.stringify(join(root, 'package.json'))});`
+ },
+ outfile: output
+ })
+ const { waitForSentinel, RELAY_SENTINEL } = createRequire(import.meta.url)(output)
+ const held = []
+ const banners = []
+ for (let i = 0; i < 100; i++) {
+ const channel = Object.assign(new EventEmitter(), {
+ stderr: new EventEmitter(),
+ stdin: { write: () => true },
+ close: () => {}
+ })
+ const pending = waitForSentinel(channel)
+ banners.push(feedBanner(channel))
+ channel.emit('data', Buffer.from(RELAY_SENTINEL))
+ const transport = await pending
+ const received = []
+ transport.onData((bytes) => received.push(bytes.toString()))
+ channel.emit('data', Buffer.from('frame'))
+ assert.deepEqual(received, ['frame'])
+ held.push({ channel, transport })
+ }
+ await new Promise((resolve) => setImmediate(resolve))
+ for (let i = 0; i < 5; i++) {
+ global.gc()
+ }
+ const retained = banners.filter((reference) => reference.deref() !== undefined).length
+ console.log(
+ JSON.stringify({
+ connections: held.length,
+ bannerBytes: 65536,
+ retainedBannerBuffers: retained,
+ retainedBannerBytes: retained * 65536
+ })
+ )
+} finally {
+ await rm(directory, { recursive: true, force: true })
+}
+
+function feedBanner(channel) {
+ const banner = Buffer.alloc(65536, 120)
+ channel.emit('data', banner)
+ return new WeakRef(banner.buffer)
+}
diff --git a/config/scripts/benchmark-skill-depth.mjs b/config/scripts/benchmark-skill-depth.mjs
new file mode 100644
index 00000000000..1ebb606f93c
--- /dev/null
+++ b/config/scripts/benchmark-skill-depth.mjs
@@ -0,0 +1,122 @@
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import * as fs from 'node:fs/promises'
+import Module from 'node:module'
+import { tmpdir } from 'node:os'
+import { join, resolve } from 'node:path'
+import { performance } from 'node:perf_hooks'
+import { build } from 'esbuild'
+
+// Pass a pre-change skill-root-file-walk.ts snapshot as the only argument.
+const baselinePath = process.argv[2]
+const brokenLinks = process.argv.includes('--broken')
+assert.ok(baselinePath, 'Pass a pre-change skill-root-file-walk.ts snapshot.')
+const entry = 'src/main/skills/skill-root-file-walk.ts'
+const baseline = readFileSync(baselinePath, 'utf8')
+assert.notEqual(baseline, readFileSync(entry, 'utf8'), 'Do not compare the source to itself.')
+let statCalls = 0
+
+async function load(useBaseline) {
+ const result = await build({
+ entryPoints: [entry],
+ bundle: true,
+ platform: 'node',
+ format: 'cjs',
+ write: false,
+ logLevel: 'silent',
+ plugins: useBaseline
+ ? [
+ {
+ name: 'baseline-skill-depth',
+ setup(builder) {
+ builder.onLoad({ filter: /skill-root-file-walk\.ts$/ }, () => ({
+ contents: baseline,
+ loader: 'ts'
+ }))
+ }
+ }
+ ]
+ : []
+ })
+ const module = new Module(resolve('skill-depth-benchmark.cjs'))
+ module.paths = Module._nodeModulePaths(process.cwd())
+ const originalRequire = module.require.bind(module)
+ module.require = (name) =>
+ name === 'node:fs/promises'
+ ? {
+ ...fs,
+ stat: (...args) => {
+ statCalls++
+ return fs.stat(...args)
+ }
+ }
+ : originalRequire(name)
+ module._compile(result.outputFiles[0].text, module.id)
+ return module.exports.findSkillFiles
+}
+
+const before = await load(true)
+const after = await load(false)
+const median = (values) => values.sort((a, b) => a - b)[Math.floor(values.length / 2)]
+const temporaryRoot = await fs.mkdtemp(join(tmpdir(), 'orca-skill-depth-benchmark-'))
+try {
+ for (const links of [0, 8, 100, 1000]) {
+ const root = join(temporaryRoot, String(links))
+ const edge = join(root, 'a', 'b', 'c', 'd')
+ const target = join(temporaryRoot, 'target')
+ await fs.mkdir(edge, { recursive: true })
+ await fs.mkdir(target, { recursive: true })
+ await fs.writeFile(join(target, 'SKILL.md'), 'skill')
+ await fs.writeFile(join(edge, 'SKILL.md'), 'edge')
+ for (let index = 0; index < links; index++) {
+ await fs.symlink(
+ brokenLinks ? join(target, 'missing') : target,
+ join(edge, `link${index}`),
+ process.platform === 'win32' ? 'junction' : 'dir'
+ )
+ }
+ for (const depth of [4, 5]) {
+ const timings = { before: [], after: [] }
+ const counts = {}
+ let rows
+ for (let sample = 0; sample < 13; sample++) {
+ const versions =
+ sample % 2
+ ? [
+ ['after', after],
+ ['before', before]
+ ]
+ : [
+ ['before', before],
+ ['after', after]
+ ]
+ for (const [name, walk] of versions) {
+ statCalls = 0
+ const start = performance.now()
+ const result = await walk(root, depth)
+ const elapsed = performance.now() - start
+ if (rows) {
+ assert.deepEqual(result, rows)
+ }
+ rows = result
+ counts[name] = statCalls
+ if (sample >= 2) {
+ timings[name].push(elapsed)
+ }
+ }
+ }
+ console.log(
+ JSON.stringify({
+ links,
+ brokenLinks,
+ depth,
+ statCalls: counts,
+ rows: rows.length,
+ medianMs: { before: median(timings.before), after: median(timings.after) }
+ })
+ )
+ }
+ }
+} finally {
+ await fs.rm(temporaryRoot, { recursive: true, force: true })
+}
diff --git a/config/scripts/benchmark-tab-group-repair.mjs b/config/scripts/benchmark-tab-group-repair.mjs
new file mode 100644
index 00000000000..17a1161fc4c
--- /dev/null
+++ b/config/scripts/benchmark-tab-group-repair.mjs
@@ -0,0 +1,80 @@
+import { strict as assert } from 'node:assert'
+import { mkdtemp, readFile, rm } from 'node:fs/promises'
+import { createRequire } from 'node:module'
+import { tmpdir } from 'node:os'
+import { join, resolve } from 'node:path'
+import { performance } from 'node:perf_hooks'
+import { build } from 'esbuild'
+
+const root = resolve(import.meta.dirname, '../..')
+const source = join(root, 'src/renderer/src/store/slices/tab-group-reference-repair.ts')
+const directory = await mkdtemp(join(tmpdir(), 'orca-tab-repair-'))
+const current = await readFile(source, 'utf8')
+const indexed = `const orderedTabIds = new Set(group.tabOrder)
+ const missingTabIds = ownedTabIds.filter((tabId) => !orderedTabIds.has(tabId))`
+assert(current.includes(indexed), 'Expected indexed implementation')
+try {
+ const implementations = []
+ for (const baseline of [true, false]) {
+ const outfile = join(directory, baseline ? 'before.cjs' : 'after.cjs')
+ await build({
+ stdin: {
+ contents: baseline
+ ? current.replace(
+ indexed,
+ 'const missingTabIds = ownedTabIds.filter((tabId) => !group.tabOrder.includes(tabId))'
+ )
+ : current,
+ resolveDir: resolve(source, '..'),
+ loader: 'ts'
+ },
+ bundle: true,
+ platform: 'node',
+ format: 'cjs',
+ outfile,
+ alias: { '@': join(root, 'src/renderer/src') }
+ })
+ implementations.push(createRequire(import.meta.url)(outfile).appendOwnedTabIdsToGroups)
+ }
+ const rows = []
+ for (const count of [1, 10, 100, 1_000, 10_000]) {
+ for (const missing of [false, true]) {
+ const ids = Array.from({ length: count }, (_, i) => `tab-${i}`)
+ const groups = [
+ { id: 'group', worktreeId: 'workspace', activeTabId: null, tabOrder: ids, recentTabIds: [] }
+ ]
+ const owners = new Map(ids.map((id) => [missing ? `missing-${id}` : id, 'group']))
+ assert.deepEqual(implementations[0](groups, owners), implementations[1](groups, owners))
+ const iterations = Math.max(1, Math.floor(10_000 / count))
+ const samples = [[], []]
+ for (let sample = -3; sample < 11; sample++) {
+ for (const index of sample % 2 === 0 ? [0, 1] : [1, 0]) {
+ const start = performance.now()
+ for (let i = 0; i < iterations; i++) {
+ implementations[index](groups, owners)
+ }
+ const elapsed = (performance.now() - start) / iterations
+ if (sample >= 0) {
+ samples[index].push(elapsed)
+ }
+ }
+ }
+ rows.push({
+ count,
+ missing,
+ iterations,
+ beforeMs: samples[0].sort((a, b) => a - b)[5],
+ afterMs: samples[1].sort((a, b) => a - b)[5]
+ })
+ }
+ }
+ console.log(
+ JSON.stringify(
+ { node: process.version, platform: process.platform, samples: 11, warmups: 3, rows },
+ null,
+ 2
+ )
+ )
+} finally {
+ await rm(directory, { recursive: true, force: true })
+}
diff --git a/config/scripts/benchmark-transcript-reverse-lines.mjs b/config/scripts/benchmark-transcript-reverse-lines.mjs
new file mode 100644
index 00000000000..e9d370d1839
--- /dev/null
+++ b/config/scripts/benchmark-transcript-reverse-lines.mjs
@@ -0,0 +1,124 @@
+import assert from 'node:assert/strict'
+import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import Module from 'node:module'
+import { tmpdir } from 'node:os'
+import { join, resolve } from 'node:path'
+import { performance } from 'node:perf_hooks'
+import { build } from 'esbuild'
+
+const entry = 'src/shared/agent-hook-listener/transcript-reader.ts'
+assert.ok(process.argv[2], 'Pass a pre-change transcript-reader.ts snapshot.')
+const baseline = readFileSync(process.argv[2], 'utf8')
+assert.notEqual(baseline, readFileSync(entry, 'utf8'), 'Do not compare the source to itself.')
+
+async function load(useBaseline) {
+ const result = await build({
+ stdin: {
+ contents: `export * from './${entry}';
+export { extractAssistantTextFromLine } from './src/shared/agent-hook-listener/transcript-entry-text.ts';`,
+ resolveDir: process.cwd(),
+ loader: 'ts'
+ },
+ bundle: true,
+ platform: 'node',
+ format: 'cjs',
+ write: false,
+ logLevel: 'silent',
+ plugins: useBaseline
+ ? [
+ {
+ name: 'baseline-transcript-reader',
+ setup(builder) {
+ builder.onLoad({ filter: /transcript-reader\.ts$/ }, () => ({
+ contents: baseline,
+ loader: 'ts'
+ }))
+ }
+ }
+ ]
+ : []
+ })
+ const module = new Module(resolve('transcript-benchmark.cjs'))
+ module.paths = Module._nodeModulePaths(process.cwd())
+ module._compile(result.outputFiles[0].text, module.id)
+ return module.exports
+}
+
+const versions = [await load(true), await load(false)]
+function measure(functions, iterations) {
+ let sink = 0
+ const run = (fn) => {
+ for (let i = 0; i < iterations; i++) {
+ sink += fn()?.length ?? 0
+ }
+ }
+ for (const fn of functions) {
+ for (let i = 0; i < 3; i++) {
+ run(fn)
+ }
+ }
+ const samples = [[], []]
+ for (let round = 0; round < 11; round++) {
+ for (const index of round % 2 ? [1, 0] : [0, 1]) {
+ const start = performance.now()
+ run(functions[index])
+ samples[index].push((performance.now() - start) / iterations)
+ }
+ }
+ return {
+ beforeMs: samples[0].sort((a, b) => a - b)[5],
+ afterMs: samples[1].sort((a, b) => a - b)[5],
+ iterations,
+ sink
+ }
+}
+
+const cases = [
+ ['tiny', `${JSON.stringify({ role: 'assistant', content: 'hello' })}\n`, 10000],
+ ['64KiB line', `${JSON.stringify({ role: 'assistant', content: 'x'.repeat(65500) })}\n`, 100],
+ [
+ '4MiB line',
+ `${JSON.stringify({ role: 'assistant', content: 'x'.repeat(4 * 1024 * 1024 - 40) })}\n`,
+ 10
+ ],
+ [
+ '1000 short tool lines',
+ Array.from({ length: 1000 }, () =>
+ JSON.stringify({ role: 'tool', content: 'x'.repeat(100) })
+ ).join('\n'),
+ 50
+ ],
+ [
+ 'Unicode line',
+ `${JSON.stringify({ role: 'assistant', content: '😀漢字'.repeat(16000) })}\n`,
+ 100
+ ],
+ [
+ 'leading and trailing blank lines',
+ `\n\r\n${JSON.stringify({ role: 'assistant', content: 'hello' })}\n\n`,
+ 10000
+ ]
+]
+const directory = mkdtempSync(join(tmpdir(), 'orca-transcript-benchmark-'))
+try {
+ for (const [name, text, iterations] of cases) {
+ const file = join(directory, 'transcript.jsonl')
+ writeFileSync(file, text)
+ const scanners = versions.map(
+ (v) => () => v.findLastExtractedTranscriptLineText(text, v.extractAssistantTextFromLine)
+ )
+ const readers = versions.map((v) => () => v.readLastAssistantFromTranscriptOnce(file))
+ assert.equal(scanners[0](), scanners[1](), name)
+ assert.equal(readers[0](), readers[1](), name)
+ console.log(
+ JSON.stringify({
+ name,
+ bytes: Buffer.byteLength(text),
+ scanner: measure(scanners, iterations),
+ warmFileReader: measure(readers, Math.min(iterations, 100))
+ })
+ )
+ }
+} finally {
+ rmSync(directory, { recursive: true, force: true })
+}
diff --git a/config/scripts/cli-runtime-client-deferral-equivalence.mjs b/config/scripts/cli-runtime-client-deferral-equivalence.mjs
index f443bf3b9f9..a231b5ba75a 100644
--- a/config/scripts/cli-runtime-client-deferral-equivalence.mjs
+++ b/config/scripts/cli-runtime-client-deferral-equivalence.mjs
@@ -2,7 +2,7 @@
// Equivalence check for deferring the RuntimeClient module graph in the CLI.
//
// Builds the CLI twice with the REAL tsc emit — once from the working tree and
-// once with the seven touched files restored from git HEAD~ (the pre-deferral
+// once with the touched files restored from git HEAD~ (the pre-deferral
// implementation) — then compares stdout, stderr and exit code BYTE FOR BYTE
// across a matrix of invocations.
//
@@ -13,7 +13,7 @@
//
// Usage: node config/scripts/cli-runtime-client-deferral-equivalence.mjs [--baseline ]
import { execFileSync, spawnSync } from 'node:child_process'
-import { mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync } from 'node:fs'
+import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -21,8 +21,11 @@ const REPO = fileURLToPath(new URL('../..', import.meta.url))
// The files this change touches. Restoring exactly these from the baseline rev
// reconstructs the old implementation without disturbing anything else.
+// Files absent at the baseline (e.g. cli-error.ts, split out of format.ts
+// later) are removed for the baseline build and put back afterwards.
const TOUCHED = [
'src/cli/args.ts',
+ 'src/cli/cli-error.ts',
'src/cli/dispatch.ts',
'src/cli/flags.ts',
'src/cli/format.ts',
@@ -72,12 +75,16 @@ function buildTree(label, baselineRev) {
if (baselineRev) {
for (const file of TOUCHED) {
const path = join(REPO, file)
- restored.push([path, readFileSync(path)])
- const old = execFileSync('git', ['show', `${baselineRev}:${file}`], {
+ restored.push([path, existsSync(path) ? readFileSync(path) : null])
+ const old = spawnSync('git', ['show', `${baselineRev}:${file}`], {
cwd: REPO,
maxBuffer: 64 * 1024 * 1024
})
- writeFileSync(path, old)
+ if (old.status === 0) {
+ writeFileSync(path, old.stdout)
+ } else {
+ rmSync(path, { force: true })
+ }
}
}
execFileSync(
@@ -97,7 +104,11 @@ function buildTree(label, baselineRev) {
)
} finally {
for (const [path, contents] of restored) {
- writeFileSync(path, contents)
+ if (contents === null) {
+ rmSync(path, { force: true })
+ } else {
+ writeFileSync(path, contents)
+ }
}
}
return join(outDir, 'cli/index.js')
diff --git a/config/scripts/electron-builder-markdown-associations.test.mjs b/config/scripts/electron-builder-markdown-associations.test.mjs
index 7ae3b1c9428..58f6f8d8865 100644
--- a/config/scripts/electron-builder-markdown-associations.test.mjs
+++ b/config/scripts/electron-builder-markdown-associations.test.mjs
@@ -103,14 +103,24 @@ describe('electron-builder markdown file associations', () => {
// Why: this include was renamed from daemon-host-uninstall.nsh to carry the markdown
// hooks too. electron-builder allows only one include, so a merge that drops the daemon
- // sweep would silently orphan a running orca-terminal-daemon.exe on every uninstall.
+ // sweep would silently orphan a running daemon host on every uninstall.
+ //
+ // Asserted against comment-stripped script, and on the app exe name first: the relocated
+ // host is a verbatim copy of the app exe (daemonHostExeName, daemon-host-relocation.ts),
+ // so a macro that kills only orca-terminal-daemon.exe matches no running process. The
+ // prose above the macro names both, so a toContain over the raw file proves nothing.
it('keeps the daemon-host uninstall sweep across the include rename', async () => {
- const hooks = await readInstallerHooks()
+ const script = stripNsisCommentLines(await readInstallerHooks())
- expect(hooks).toContain('orca-terminal-daemon.exe')
- expect(hooks).toContain('$LOCALAPPDATA\\Orca\\daemon-host')
+ expect(script).toMatch(/taskkill[^\n]*\/IM\s+"?\$\{APP_EXECUTABLE_FILENAME\}"?/)
+ // Legacy name, so hosts left by builds that renamed the copy still get reaped.
+ expect(script).toMatch(/taskkill[^\n]*\/IM\s+"?orca-terminal-daemon\.exe"?/)
+ // Scopes both kills to the uninstalling user: an elevated machine-wide uninstall must
+ // not reach another logged-on user's session.
+ expect(script).toMatch(/\/FI\s+"USERNAME eq /)
+ expect(script).toContain('$LOCALAPPDATA\\Orca\\daemon-host')
// Without this guard, uninstallOldVersion would kill the daemon on every update —
// defeating the relocation that keeps terminals alive across updates.
- expect(hooks).toMatch(/\$\{ifNot\}\s+\$\{isUpdated\}/)
+ expect(script).toMatch(/\$\{ifNot\}\s+\$\{isUpdated\}/)
})
})
diff --git a/config/scripts/file-explorer-deletion-roots-benchmark.mjs b/config/scripts/file-explorer-deletion-roots-benchmark.mjs
new file mode 100644
index 00000000000..d276964861b
--- /dev/null
+++ b/config/scripts/file-explorer-deletion-roots-benchmark.mjs
@@ -0,0 +1,75 @@
+import assert from 'node:assert/strict'
+import { join } from 'node:path'
+import { performance } from 'node:perf_hooks'
+import { fileURLToPath } from 'node:url'
+import { build } from 'esbuild'
+
+const root = fileURLToPath(new URL('../..', import.meta.url))
+const bundled = await build({
+ stdin: {
+ contents: `export { selectDeletionRoots } from './file-explorer-batch-deletion';
+ export { isPathEqualOrDescendant } from './file-explorer-paths';`,
+ resolveDir: join(root, 'src/renderer/src/components/right-sidebar'),
+ loader: 'ts'
+ },
+ alias: { '@': join(root, 'src/renderer/src') },
+ bundle: true,
+ platform: 'node',
+ format: 'esm',
+ write: false,
+ logLevel: 'silent'
+})
+const { selectDeletionRoots, isPathEqualOrDescendant } = await import(
+ `data:text/javascript;base64,${Buffer.from(bundled.outputFiles[0].text).toString('base64')}`
+)
+
+// Original production selector; both paths use the same path-comparison implementation.
+function original(nodes) {
+ return nodes.filter(
+ (n) =>
+ !nodes.some(
+ (other) => other !== n && other.isDirectory && isPathEqualOrDescendant(n.path, other.path)
+ )
+ )
+}
+
+function measure(run, nodes) {
+ for (let index = 0; index < 3; index++) {
+ run(nodes)
+ }
+ const samples = []
+ for (let index = 0; index < 11; index++) {
+ const start = performance.now()
+ run(nodes)
+ samples.push(performance.now() - start)
+ }
+ return samples.sort((a, b) => a - b)[5]
+}
+
+const results = []
+for (const [fileCount, directoryCount] of [
+ [100, 0],
+ [1000, 0],
+ [5000, 0],
+ [5000, 5],
+ [0, 100]
+]) {
+ const nodes = Array.from({ length: fileCount + directoryCount }, (_, index) => ({
+ name: `item-${index}`,
+ path: `/repo/item-${index}`,
+ relativePath: `item-${index}`,
+ isDirectory: index >= fileCount,
+ depth: 0
+ }))
+ const expected = original(nodes)
+ const actual = selectDeletionRoots(nodes)
+ assert.equal(actual.length, expected.length)
+ actual.forEach((node, index) => assert.equal(node, expected[index]))
+ results.push({
+ fileCount,
+ directoryCount,
+ beforeMs: measure(original, nodes),
+ afterMs: measure(selectDeletionRoots, nodes)
+ })
+}
+console.log(JSON.stringify({ node: process.version, platform: process.platform, results }, null, 2))
diff --git a/config/scripts/mobile-file-ranking-benchmark.mjs b/config/scripts/mobile-file-ranking-benchmark.mjs
new file mode 100644
index 00000000000..68ac5b9d977
--- /dev/null
+++ b/config/scripts/mobile-file-ranking-benchmark.mjs
@@ -0,0 +1,53 @@
+import assert from 'node:assert/strict'
+import { execFileSync } from 'node:child_process'
+import { readFileSync } from 'node:fs'
+import { stripTypeScriptTypes } from 'node:module'
+import { performance } from 'node:perf_hooks'
+
+const baseline = process.argv[2]
+if (!baseline) {
+ throw new Error('Usage: node config/scripts/mobile-file-ranking-benchmark.mjs ')
+}
+async function load(source) {
+ const js = stripTypeScriptTypes(source, { mode: 'transform' })
+ return await import(`data:text/javascript;base64,${Buffer.from(js).toString('base64')}`)
+}
+function measure(fn, paths, query) {
+ for (let warmup = 0; warmup < 10; warmup++) {
+ fn(paths, query, 16)
+ }
+ const samples = []
+ for (let i = 0; i < 9; i++) {
+ const start = performance.now()
+ fn(paths, query, 16)
+ samples.push(performance.now() - start)
+ }
+ return samples.sort((a, b) => a - b)[4]
+}
+const results = []
+for (const [file, name] of [
+ ['src/main/runtime/runtime-mobile-file-path-search.ts', 'rankRuntimeMobileFilePaths'],
+ ['mobile/src/session/mobile-native-chat-autocomplete.ts', 'rankSuggestions']
+]) {
+ const before = (
+ await load(execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' }))
+ )[name]
+ const after = (await load(readFileSync(file, 'utf8')))[name]
+ for (const count of [100, 100000]) {
+ const paths = Array.from(
+ { length: count },
+ (_, i) => `src/components/workspace/group-${i % 100}/file-${i}.tsx`
+ )
+ for (const query of ['file-9', 'missing', 'workspace']) {
+ assert.deepEqual(after(paths, query, 16), before(paths, query, 16))
+ results.push({
+ function: name,
+ paths: count,
+ query,
+ beforeMs: measure(before, paths, query),
+ afterMs: measure(after, paths, query)
+ })
+ }
+ }
+}
+console.log(JSON.stringify({ node: process.version, platform: process.platform, results }, null, 2))
diff --git a/config/scripts/mobile-markdown-placeholder-benchmark.mjs b/config/scripts/mobile-markdown-placeholder-benchmark.mjs
new file mode 100644
index 00000000000..20280e5a8d2
--- /dev/null
+++ b/config/scripts/mobile-markdown-placeholder-benchmark.mjs
@@ -0,0 +1,58 @@
+import assert from 'node:assert/strict'
+import { execFileSync } from 'node:child_process'
+import { readFileSync } from 'node:fs'
+import { dirname, resolve } from 'node:path'
+import { performance } from 'node:perf_hooks'
+import { build } from 'esbuild'
+
+const sourcePath = 'mobile/src/components/mobile-markdown-preview-html.ts'
+const baselineRef = process.argv[2]
+if (!baselineRef) {
+ throw new Error(
+ 'Usage: node config/scripts/mobile-markdown-placeholder-benchmark.mjs '
+ )
+}
+async function load(source) {
+ const result = await build({
+ stdin: { contents: source, resolveDir: dirname(resolve(sourcePath)), loader: 'ts' },
+ bundle: true,
+ write: false,
+ platform: 'node',
+ format: 'esm'
+ })
+ return (
+ await import(
+ `data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
+ )
+ ).normalizeMobileMarkdownPreviewHtml
+}
+const before = await load(
+ execFileSync('git', ['show', `${baselineRef}:${sourcePath}`], { encoding: 'utf8' })
+)
+const after = await load(readFileSync(sourcePath, 'utf8'))
+function measure(fn, input, repeats) {
+ const samples = []
+ for (let run = 0; run < repeats; run++) {
+ const start = performance.now()
+ fn(input)
+ samples.push(performance.now() - start)
+ }
+ return samples.sort((a, b) => a - b)[Math.floor(samples.length / 2)]
+}
+const results = []
+for (const [shape, input] of [
+ ['ordinary Markdown', '# Hello\n\nUse `Array` and bold.
'],
+ ...[2048, 8192, 16384].map((length) => [
+ `${length} underscore collision`,
+ `\uE000ORCA_MD_CODE_${'_'.repeat(length)}0\uE000 and \`Array\``
+ ])
+]) {
+ assert.equal(after(input), before(input))
+ results.push({
+ shape,
+ bytes: Buffer.byteLength(input),
+ beforeMs: measure(before, input, 5),
+ afterMs: measure(after, input, 15)
+ })
+}
+console.log(JSON.stringify({ node: process.version, platform: process.platform, results }, null, 2))
diff --git a/config/scripts/pr-e2e-native-only-routing.test.mjs b/config/scripts/pr-e2e-native-only-routing.test.mjs
new file mode 100644
index 00000000000..b6c3662cd1d
--- /dev/null
+++ b/config/scripts/pr-e2e-native-only-routing.test.mjs
@@ -0,0 +1,33 @@
+import { readFileSync } from 'node:fs'
+import { describe, expect, it } from 'vitest'
+import { parse } from 'yaml'
+import { hasNativeImeSourceChange, shouldRunReusablePrE2e } from './pr-e2e-source-routing.mjs'
+
+const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
+const filterStep = workflow.jobs.code_paths.steps.find((step) => step.id === 'e2e_filter')
+
+describe('native-only PR E2E routing', () => {
+ it('avoids generic E2E allocation for native-only changes while preserving its IME lane', () => {
+ for (const file of [
+ 'tests/e2e/terminal-ibus-hangul-native.spec.ts',
+ 'config/scripts/run-terminal-ibus-hangul-e2e.mjs'
+ ]) {
+ expect(hasNativeImeSourceChange([file])).toBe(true)
+ expect(shouldRunReusablePrE2e([file])).toBe(false)
+ }
+ expect(shouldRunReusablePrE2e([])).toBe(false)
+ for (const spec of [
+ 'tests/e2e/ssh-startup-exec-readiness.spec.ts',
+ 'tests/e2e/paired-startup-exec-readiness.spec.ts',
+ 'tests/e2e/terminal-ime-exact-byte.spec.ts',
+ 'tests/e2e/future.spec.ts'
+ ]) {
+ expect(shouldRunReusablePrE2e([spec])).toBe(true)
+ expect(shouldRunReusablePrE2e(['tests/e2e/terminal-ibus-hangul-native.spec.ts', spec])).toBe(
+ true
+ )
+ }
+ expect(filterStep.run).toContain('pr-e2e-source-routing.mjs --reusable-workflow')
+ expect(filterStep.run).toContain('if [ "$SHOULD_RUN" = true ]; then')
+ })
+})
diff --git a/config/scripts/pr-e2e-source-routing.mjs b/config/scripts/pr-e2e-source-routing.mjs
index 78814b663cb..5b698fb0b42 100644
--- a/config/scripts/pr-e2e-source-routing.mjs
+++ b/config/scripts/pr-e2e-source-routing.mjs
@@ -217,6 +217,16 @@ export function hasNativeImeSourceChange(changedPaths) {
).some((route) => changedPaths.some(route.matches))
}
+export function shouldRunReusablePrE2e(changedPaths) {
+ // Native IME has its own workflow; SSH still runs inside the reusable workflow.
+ return (
+ hasSshSourceChange(changedPaths) ||
+ selectPrE2eSpecs(changedPaths).some(
+ (spec) => spec !== 'tests/e2e/terminal-ibus-hangul-native.spec.ts'
+ )
+ )
+}
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
let input = ''
process.stdin.setEncoding('utf8')
@@ -226,6 +236,8 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
const changedPaths = input.split(/\r?\n/).filter(Boolean)
if (process.argv.includes('--ssh-source')) {
process.stdout.write(`${hasSshSourceChange(changedPaths)}\n`)
+ } else if (process.argv.includes('--reusable-workflow')) {
+ process.stdout.write(`${shouldRunReusablePrE2e(changedPaths)}\n`)
} else if (process.argv.includes('--native-ime-source')) {
process.stdout.write(`${hasNativeImeSourceChange(changedPaths)}\n`)
} else {
diff --git a/config/scripts/quick-open-exclusion-benchmark.mjs b/config/scripts/quick-open-exclusion-benchmark.mjs
new file mode 100644
index 00000000000..399302c5a2b
--- /dev/null
+++ b/config/scripts/quick-open-exclusion-benchmark.mjs
@@ -0,0 +1,60 @@
+import assert from 'node:assert/strict'
+import { performance } from 'node:perf_hooks'
+import { build } from 'esbuild'
+
+const bundled = await build({
+ entryPoints: ['src/shared/quick-open-filter.ts'],
+ bundle: true,
+ platform: 'node',
+ format: 'esm',
+ write: false,
+ logLevel: 'silent'
+})
+const { shouldExcludeQuickOpenRelPath: after } = await import(
+ `data:text/javascript;base64,${Buffer.from(bundled.outputFiles[0].text).toString('base64')}`
+)
+// Original production predicate, including its exact boundary check.
+function before(relPath, prefixes) {
+ for (const prefix of prefixes) {
+ if (relPath === prefix) {
+ return true
+ }
+ if (relPath.length > prefix.length && relPath.startsWith(`${prefix}/`)) {
+ return true
+ }
+ }
+ return false
+}
+const files = Array.from(
+ { length: 100000 },
+ (_, index) => `src/components/group-${index % 100}/file-${index}.tsx`
+)
+function run(fn, prefixes) {
+ let excluded = 0
+ for (const file of files) {
+ excluded += Number(fn(file, prefixes))
+ }
+ return excluded
+}
+function measure(fn, prefixes) {
+ run(fn, prefixes)
+ const samples = []
+ for (let index = 0; index < 5; index++) {
+ const start = performance.now()
+ run(fn, prefixes)
+ samples.push(performance.now() - start)
+ }
+ return samples.sort((a, b) => a - b)[2]
+}
+const results = []
+for (const count of [0, 10, 100, 500]) {
+ const prefixes = Array.from({ length: count }, (_, index) => `nested-worktrees/worktree-${index}`)
+ assert.equal(run(after, prefixes), run(before, prefixes))
+ results.push({
+ files: files.length,
+ exclusions: count,
+ beforeMs: measure(before, prefixes),
+ afterMs: measure(after, prefixes)
+ })
+}
+console.log(JSON.stringify({ node: process.version, platform: process.platform, results }, null, 2))
diff --git a/config/scripts/relay-frame-buffer-benchmark.mjs b/config/scripts/relay-frame-buffer-benchmark.mjs
new file mode 100644
index 00000000000..24d7b565400
--- /dev/null
+++ b/config/scripts/relay-frame-buffer-benchmark.mjs
@@ -0,0 +1,62 @@
+#!/usr/bin/env node
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import { stripTypeScriptTypes } from 'node:module'
+import { performance } from 'node:perf_hooks'
+
+// Pass the pre-change source saved with git show :src/shared/relay-frame-buffer.ts.
+const baselinePath = process.argv[2]
+if (!baselinePath) {
+ throw new Error('Usage: node config/scripts/relay-frame-buffer-benchmark.mjs ')
+}
+async function load(source) {
+ return (
+ await import(
+ `data:text/javascript;base64,${Buffer.from(stripTypeScriptTypes(source)).toString('base64')}`
+ )
+ ).RelayFrameBuffer
+}
+const Before = await load(readFileSync(baselinePath, 'utf8'))
+const After = await load(
+ readFileSync(new URL('../../src/shared/relay-frame-buffer.ts', import.meta.url), 'utf8')
+)
+function median(values) {
+ return values.sort((a, b) => a - b)[Math.floor(values.length / 2)]
+}
+for (const count of [1, 256, 16384, 65536]) {
+ const chunks = Array.from({ length: count }, (_, index) => Buffer.alloc(64, index % 256))
+ const expected = Buffer.concat(chunks)
+ for (const mode of ['take', 'discard']) {
+ const times = [[], []]
+ for (let round = 0; round < 9; round += 1) {
+ for (const arm of round % 2 === 0 ? [0, 1] : [1, 0]) {
+ const FrameBuffer = arm === 0 ? Before : After
+ const buffer = new FrameBuffer()
+ for (const chunk of chunks) {
+ buffer.append(chunk)
+ }
+ const start = performance.now()
+ const output = buffer[mode](expected.length)
+ times[arm].push(performance.now() - start)
+ if (mode === 'take') {
+ assert.deepEqual(output, expected)
+ }
+ assert.equal(buffer.length, 0)
+ buffer.append(Buffer.from('tail'))
+ assert.equal(buffer.drain().toString(), 'tail')
+ }
+ }
+ const beforeMs = median(times[0]),
+ afterMs = median(times[1])
+ console.log(
+ JSON.stringify({
+ mode,
+ chunks: count,
+ bytes: expected.length,
+ beforeMs,
+ afterMs,
+ speedup: beforeMs / afterMs
+ })
+ )
+ }
+}
diff --git a/config/scripts/repo-icon-source-href-benchmark.mjs b/config/scripts/repo-icon-source-href-benchmark.mjs
new file mode 100644
index 00000000000..76c42d261b4
--- /dev/null
+++ b/config/scripts/repo-icon-source-href-benchmark.mjs
@@ -0,0 +1,55 @@
+import assert from 'node:assert/strict'
+import { performance } from 'node:perf_hooks'
+import { extractIconHref } from '../../src/main/repo-icon-source-href.ts'
+
+// Original production expressions, preserved for the before/after measurement.
+const html =
+ /]*\brel=["'](?:icon|shortcut icon)["'])(?=[^>]*\bhref=["']([^"'?]+))[^>]*>/i
+const object =
+ /(?=[^}]*\brel\s*:\s*["'](?:icon|shortcut icon)["'])(?=[^}]*\bhref\s*:\s*["']([^"'?]+))[^}]*/i
+const original = (source) => source.match(html)?.[1] ?? source.match(object)?.[1] ?? null
+
+function measurePair(source) {
+ original(source)
+ extractIconHref(source)
+ const beforeSamples = []
+ const afterSamples = []
+ for (let run = 0; run < 5; run++) {
+ const measurements = [
+ [original, beforeSamples],
+ [extractIconHref, afterSamples]
+ ]
+ if (run % 2 === 1) {
+ measurements.reverse()
+ }
+ for (const [fn, samples] of measurements) {
+ const started = performance.now()
+ fn(source)
+ samples.push(performance.now() - started)
+ }
+ }
+ return {
+ beforeMs: beforeSamples.sort((a, b) => a - b)[2],
+ afterMs: afterSamples.sort((a, b) => a - b)[2]
+ }
+}
+
+const results = []
+for (const size of [8192, 16384, 32768]) {
+ for (const shape of ['no icon', 'rel without href', 'unterminated link starts']) {
+ const source =
+ shape === 'unterminated link starts'
+ ? '')
+}
+const source = execFileSync('git', ['show', `${ref}:src/shared/source-scan/source-tree-scan.ts`], {
+ encoding: 'utf8'
+})
+const { blankStringContents: before } = await import(
+ `data:text/javascript;base64,${Buffer.from(stripTypeScriptTypes(source)).toString('base64')}`
+)
+const tokens = [
+ 'a',
+ '/',
+ '*',
+ ' ',
+ '\n',
+ '\r',
+ '\t',
+ '\u00a0',
+ '\u2028',
+ '"',
+ "'",
+ '`',
+ '${',
+ '}',
+ '{',
+ '\\',
+ '(',
+ ')',
+ '[',
+ ']',
+ '=',
+ '+',
+ '-',
+ ';'
+]
+let seed = 173
+for (let sample = 0; sample < 3000; sample++) {
+ let input = ''
+ for (let token = 0; token < 40; token++) {
+ seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
+ input += tokens[seed % tokens.length]
+ }
+ assert.equal(after(input), before(input), JSON.stringify(input))
+ assert.equal(after(input, true), before(input, true), JSON.stringify(input))
+}
+function measure(fn, input) {
+ const samples = []
+ for (let run = 0; run < 3; run++) {
+ const start = performance.now()
+ fn(input)
+ samples.push(performance.now() - start)
+ }
+ return samples.sort((a, b) => a - b)[1]
+}
+const results = []
+for (const lines of [100, 1000, 5000, 10000]) {
+ const input = 'const x = value / 2;\n'.repeat(lines)
+ assert.equal(after(input), before(input))
+ results.push({
+ lines,
+ bytes: Buffer.byteLength(input),
+ beforeMs: measure(before, input),
+ afterMs: measure(after, input)
+ })
+}
+console.log(
+ JSON.stringify(
+ { node: process.version, platform: process.platform, differentialCases: 3000, results },
+ null,
+ 2
+ )
+)
diff --git a/config/scripts/workflow-ref-mirror-case-safety.test.mjs b/config/scripts/workflow-ref-mirror-case-safety.test.mjs
new file mode 100644
index 00000000000..31366f5e489
--- /dev/null
+++ b/config/scripts/workflow-ref-mirror-case-safety.test.mjs
@@ -0,0 +1,44 @@
+import { readFileSync } from 'node:fs'
+import { join, resolve } from 'node:path'
+import { describe, expect, it } from 'vitest'
+import { parse } from 'yaml'
+
+const projectDir = resolve(import.meta.dirname, '../..')
+
+const readWorkflow = (relativePath) => parse(readFileSync(join(projectDir, relativePath), 'utf8'))
+
+// Every step that mirrors this repo's whole ref namespace onto a runner disk to
+// prove a commit is reachable from a branch or tag before signing it.
+const REF_MIRRORS = [
+ ['.github/workflows/adhoc-mac-build.yml', 'build-adhoc-mac', 'Vet the requested ref'],
+ ['.github/workflows/dev-channel-win-build.yml', 'build-win', 'Vet the requested inputs']
+]
+
+describe('ref-mirroring vet steps', () => {
+ it('keeps the full-history adhoc checkout on the same case-safe backend', () => {
+ const steps = readWorkflow('.github/workflows/adhoc-mac-build.yml').jobs['build-adhoc-mac']
+ .steps
+ const checkout = steps.find((step) => step.name === 'Checkout the requested ref')
+ expect(checkout.env.GIT_DEFAULT_REF_FORMAT).toBe('reftable')
+ expect(checkout.with.ref).toBe('${{ steps.vetted.outputs.sha }}')
+ expect(checkout.with['fetch-depth']).toBe(0)
+ expect(checkout.with['persist-credentials']).toBe(false)
+ })
+
+ // Why: macOS and Windows runner disks are case-insensitive, and this repo has
+ // branches that differ only in casing. The files backend cannot store both, and
+ // it fails the whole fetch rather than the one ref — so the vet step dies before
+ // any build runs. reftable keys refs in a table instead of file paths.
+ it.each(REF_MIRRORS)(
+ '%s creates its scratch repo with the reftable backend',
+ (path, job, step) => {
+ const run = readWorkflow(path).jobs[job].steps.find(
+ (candidate) => candidate.name === step
+ ).run
+
+ expect(run).toContain('+refs/heads/*:refs/heads/*')
+ expect(run).toMatch(/git init\b[^\n]*--ref-format=reftable/)
+ expect(run).not.toMatch(/git init -q --bare "\$scratch"/)
+ }
+ )
+})
diff --git a/config/scripts/workflow-ref-reachability.test.mjs b/config/scripts/workflow-ref-reachability.test.mjs
new file mode 100644
index 00000000000..d71c3094c56
--- /dev/null
+++ b/config/scripts/workflow-ref-reachability.test.mjs
@@ -0,0 +1,125 @@
+import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { pathToFileURL } from 'node:url'
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
+import { parse } from 'yaml'
+import { runProcess } from '../../src/shared/child-process/run-process'
+
+const readWorkflow = (name) => parse(readFileSync(`.github/workflows/${name}.yml`, 'utf8'))
+const windowsVet = readWorkflow('dev-channel-win-build').jobs['build-win'].steps.find(
+ (step) => step.id === 'vetted'
+)
+const macSteps = readWorkflow('adhoc-mac-build').jobs['build-adhoc-mac'].steps
+const macVet = macSteps.find((step) => step.id === 'vetted')
+const macCheckout = macSteps.find((step) => step.name === 'Checkout the requested ref')
+const directory = mkdtempSync(join(tmpdir(), 'workflow-ref-reachability-'))
+const repository = join(directory, 'remote.git')
+const identity = {
+ ...process.env,
+ GIT_AUTHOR_NAME: 'Ref test',
+ GIT_AUTHOR_EMAIL: 'ref-test@example.com',
+ GIT_COMMITTER_NAME: 'Ref test',
+ GIT_COMMITTER_EMAIL: 'ref-test@example.com'
+}
+let ancestor, upper, lower, untrusted
+
+async function git(args, env = identity) {
+ const result = await runProcess({ program: 'git', args, env })
+ expect(result.code, result.stderr).toBe(0)
+ return result.stdout.trim()
+}
+
+beforeAll(async () => {
+ await git(['init', '--bare', '--ref-format=reftable', repository])
+ const tree = await git(['-C', repository, 'mktree'])
+ ancestor = await git(['-C', repository, 'commit-tree', tree, '-m', 'ancestor'])
+ upper = await git(['-C', repository, 'commit-tree', tree, '-p', ancestor, '-m', 'upper'])
+ lower = await git(['-C', repository, 'commit-tree', tree, '-p', ancestor, '-m', 'lower'])
+ untrusted = await git(['-C', repository, 'commit-tree', tree, '-m', 'PR only'])
+ for (const [ref, sha] of [
+ ['refs/heads/Fix', upper],
+ ['refs/heads/fix', lower],
+ ['refs/pull/1/head', untrusted]
+ ]) {
+ await git(['-C', repository, 'update-ref', ref, sha])
+ }
+ await git(['-C', repository, 'tag', '-a', 'Release', upper, '-m', 'upper tag'])
+ await git(['-C', repository, 'tag', '-a', 'release', lower, '-m', 'lower tag'])
+ await git(['-C', repository, 'config', 'uploadpack.allowFilter', 'true'])
+})
+
+afterAll(() => rmSync(directory, { recursive: true, force: true }))
+
+async function vet(step, ref) {
+ const scratch = mkdtempSync(join(directory, 'attempt-'))
+ const script = join(scratch, 'vet.sh')
+ writeFileSync(script, step.run)
+ return runProcess({
+ program: 'bash',
+ args: [script],
+ env: {
+ ...identity,
+ REPO_URL: pathToFileURL(repository).href,
+ RUNNER_TEMP: scratch,
+ GITHUB_OUTPUT: join(scratch, 'output'),
+ REQUESTED_REF: ref,
+ REQUESTED_SHA: ref,
+ CHANNEL: 'hourly',
+ TAG: 'v1.0.0-hourly.test',
+ VERSION: '1.0.0-hourly.test'
+ }
+ })
+}
+
+describe('release ref trust with case-twin names', () => {
+ it('accepts both branch tips, annotated tags, and their common ancestor', async () => {
+ for (const sha of [upper, lower, ancestor]) {
+ const result = await vet(windowsVet, sha)
+ expect(result.code, result.stderr).toBe(0)
+ }
+ for (const ref of ['Fix', 'fix', 'Release', 'release', ancestor]) {
+ const result = await vet(macVet, ref)
+ expect(result.code, result.stderr).toBe(0)
+ }
+ })
+
+ it('rejects PR-only commits even when the server has their objects', async () => {
+ for (const step of [windowsVet, macVet]) {
+ const result = await vet(step, untrusted)
+ expect(result.code).not.toBe(0)
+ expect(result.stdout).toContain('not reachable from any branch or tag')
+ }
+ const result = await vet(macVet, 'refs/pull/1/head')
+ expect(result.code).not.toBe(0)
+ expect(result.stdout).toContain('Refusing to build PR ref')
+ })
+
+ it('preserves both case variants in the subsequent full-history checkout', async () => {
+ const checkout = join(directory, 'checkout')
+ const env = { ...identity, ...macCheckout.env }
+ await git(['init', checkout], env)
+ await git(
+ [
+ '-C',
+ checkout,
+ 'fetch',
+ '--no-tags',
+ repository,
+ '+refs/heads/*:refs/remotes/origin/*',
+ '+refs/tags/*:refs/tags/*'
+ ],
+ env
+ )
+ await git(['-C', checkout, 'checkout', '--detach', upper], env)
+ for (const [ref, sha] of [
+ ['refs/remotes/origin/Fix', upper],
+ ['refs/remotes/origin/fix', lower],
+ ['refs/tags/Release', upper],
+ ['refs/tags/release', lower]
+ ]) {
+ expect(await git(['-C', checkout, 'rev-parse', `${ref}^{commit}`], env)).toBe(sha)
+ }
+ expect(await git(['-C', checkout, 'rev-parse', 'HEAD'], env)).toBe(upper)
+ })
+})
diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg
index fe660c42295..33ad276aa2d 100644
--- a/docs/assets/readme-downloads.svg
+++ b/docs/assets/readme-downloads.svg
@@ -1,5 +1,5 @@
-