* Revert "fix(terminal): avoid flash while restoring parked terminals (#10871)"
This reverts commit 5a6a9e0b28.
Reverted for terminal rendering regressions (flashing, lost content).
Conflict resolution preserves the forwardRef signature from #10433 and
drops the parked-presentation gating #11016 fed with its effective set.
Co-authored-by: Orca <help@stably.ai>
* Revert "fix(terminal): limit pre-paint WebGL resume to macOS (#10794)" and "fix(terminal): stop switch bold flash and Windows lag (#10692)"
This reverts commits 4681edb520 and
8f5a45401f.
#10794 was itself a partial revert of #10692, so both are reverted
together: the Windows retained-WebGL LRU and the macOS pre-paint
(layout-phase) visibility transition that survived it. Terminal
visibility resume returns to passive disposal and recreation on every
platform, and the WebGL context ceiling returns to a flat 128.
Co-authored-by: Orca <help@stably.ai>
* Revert "fix(terminal): release an abandoned synchronized-output frame on reveal (STA-2694) (#10907)"
This reverts commit 97cb32c1cc.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(perf): correct three 07-27 perf regressions
Traversal capacity cap no longer scales with worker concurrency
(#11026). retainWorkspaceSpaceScanEntry charged a traversal-wide entry
counter, so N workers each holding a listing multiplied the live charge.
At concurrency 48 a 48x2,100 tree (100,848 entries) hit the 100,000 cap
while 100x1,500 (150,100 entries, 50% more) passed, and scanLocalWorktree
treats the capacity error as terminal, reporting an intact worktree as
"Unavailable" with sizeBytes 0. The cap is now per directory listing --
the only quantity fixed by directory shape -- restoring the invariant
docs/workspace-space-scan-resource-bounds.md already states. Aggregate
live retention stays bounded by the unchanged 64 MiB byte cap.
Note: releasing each entry's charge at dispatch (the originally suggested
fix) was measured and does not help; the peak is set at admission, before
any entry is dispatched.
Repo image icons are no longer fully base64-decoded on every snapshot
publish (#11012). sanitizeRepoIcon reached decodeBase64Prefix, which
sized its buffer to the whole payload to read a 24-byte header, running
synchronously inside ipcMain.handle at a 250 ms throttle. Validation is
now memoized on source+src in a BoundedMap. Measured for 10 icons x
256 KB: 37.34 ms -> 0.67 ms per publish.
One over-long card label no longer discards the entire snapshot (#11012).
isDashboardSnapshot was all-or-nothing and dashboard-popout returned
early with no log while replaying lastSnapshot, so `orca terminal rename
--title "<1025+ chars>"` froze the pop-out board on its last good paint
with nothing surfaced. Labels are truncated at the producer, the
validator drops only the offending card, and both the rejection and the
drop are logged. The bound now lives in the shared snapshot contract so
producer and validator cannot drift.
Co-authored-by: Orca <help@stably.ai>
* fix(perf): charge a scan listing's parent path once, not per entry
The 4.1 fix made the entry cap per-listing but left the 64 MiB byte cap
charging parentPath.length for every entry in a listing. Because a
listing's entries all share one parent-path string, that multiplied the
path by the directory's width, so the byte cap measured checkout depth
rather than live heap.
The reported symptom therefore still reproduced at the production default
limits: 48 x 2,100 @ concurrency 48 raised a capacity error once the
worktree path passed ~58 characters, while the same layout at concurrency
1 succeeded. The shipped regression test could not see this because it
passes maxRetainedBytes: Number.MAX_SAFE_INTEGER, disabling the only cap
still in play. Measured at a real 65-char worktree root, 3 of the report's
4 documented layouts still failed.
The parent path is now charged once per listing, with its first entry, so
an empty listing strands no charge. Per-entry overhead is unchanged at
512 B + name, which still dominates the estimate, so the OOM protection
the original PR added is preserved.
Adds a production-default-limits case covering the report's layouts under
a deep root, plus an assertion that a short and a deep root reach the same
verdict -- the path independence docs/workspace-space-scan-resource-bounds.md
requires and which no existing test enforced.
Co-authored-by: Orca <help@stably.ai>
* fix(perf): prove the icon cache by decode count, not wall clock
The caching test asserted a per-publish millisecond budget, which failed
on CI at 5.64 ms against a 5 ms ceiling. Any threshold flakes on a loaded
box, so count real sanitizeRepoIcon entries instead: 10 repos x 20
publishes is 200 icon checks against exactly 1 decode. Added cases pin
the cache key (payload and source both re-decode; a cached image verdict
never answers for an emoji) and that a rejection is cached too.
Also drops budget.entries, which the per-listing cap left as a
traversal-wide counter no check reads -- exactly the shape a future
guard could reintroduce the concurrency bug from.
Co-authored-by: Orca <help@stably.ai>
* fix(dashboard): bound the project filter label the whole board rides on
#11042 added snapshot-level filterOptions whose project labels are
repo.displayName -- the same unbounded source this PR already bounds for
card.repoName, but one level up where dropping a card cannot recover it.
An over-long project name would fail isDashboardFilterOptions and take
the entire snapshot with it, which is the exact frozen-board failure the
per-card drop was added to end. Workspace-status labels are already
capped at 32 by workspace-statuses.ts, so only projects needed this.
Co-authored-by: Orca <help@stably.ai>
* fix(dashboard): disambiguate the repo icon cache key
The memoization key joined `source` and `src` with a space, but the
sanitizer's base64 pattern admits whitespace inside a valid `src`. A
rejected icon can therefore split the same concatenation differently and
inherit an accepted icon's cached verdict, reaching the pop-out's
`<img src>` without ever being sanitized. Length-prefix the source.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(workspace-space): serialize local disk and cap traversal memory
Prevent resource exhaustion during large workspace scans by limiting
local disk access to one concurrent `du` call and capping portable
traversal memory to 100k entries or 64 MiB per worktree. Fixes July 27
incident with 298 worktrees causing host stalls and renderer OOM.
Portable traversals now use fixed-worker iterative frames instead of
recursive promises. Capacity failures become unavailable rows. Behavior
below limits is unchanged.
* fix(workspace-space): bound concurrent SSH fallback traversals
Desktop-side SSH fallback traversals run in the main process with independent admission
budgets. Without limiting, up to six concurrent traversals could stack six 64 MiB budgets.
Cap remote fallback traversals to 2 concurrent, keeping aggregate admission at 2 × 64 MiB.
Also make capacity error messages reflect configured limits instead of hardcoded defaults.
* fix(terminal): release an abandoned synchronized-output frame on reveal
Alt-screen agent TUIs (OpenCode/OpenTUI, Codex, grok) bracket every repaint
in `?2026h … ?2026l`. Hiding a pane mid-bracket — which a worktree switch or
cold-park lands on routinely, since these brackets are written many times a
second — leaves xterm's `decPrivateModes.synchronizedOutput` latched.
RenderService.refreshRows checks that latch *before* rendering, so while it
holds, every repaint Orca owns is a no-op: the forced render-pause repaint,
the plain `refresh()` fallback, and the shared glyph-atlas rebuild all render
zero rows while the xterm buffer is perfectly correct. Release the latch at
the two reveal repaint entry points so those repaints actually paint.
Also adds an OpenCode-shaped alt-screen e2e fixture and spec. The existing
inline-TUI convergence spec covers the normal-buffer shape (live block glued
to the bottom, history scrolling into scrollback); this covers the
full-screen alternate-buffer shape, where nothing scrolls and so no row ever
self-heals through the scroll path.
Scope note: xterm arms a 1s watchdog that clears this latch on its own, so
this closes a bounded window rather than the whole STA-2694 report. The e2e
spec passes with and without the production change for that reason; the unit
tests are what pin the behavior. Refs STA-2694.
* fix(terminal): clear the render model on the plain-refocus repaint path
`schedulePaneRevealPresent` — the atlas-preserving path a plain window
refocus takes — only called `terminal.refresh()`. xterm's renderers are
diff-based: `_updateModel` early-continues on any cell whose code/fg/bg/ext
still match the cached model, so a refresh repaints nothing for a pane whose
buffer never changed. When an occluded window loses its canvas contents while
that model stays populated, the refresh skips exactly the cells that went
stale and the pane keeps compositing pre-hide pixels — until a window resize
reallocates the model, which is the repair users find by hand.
Clear the model first (`RenderService.clear()` → renderer `clear()` →
`_clearModel(true)`) so the refresh becomes a guaranteed full repaint. That
drops cached cells and glyph vertices but NOT the texture atlas, which is
shared by every same-config terminal and whose mid-stream wipe re-arms xterm's
page-merge garble race (xterm.js #4480) — the reason this path is
atlas-preserving in the first place.
Also covers the DOM-renderer fallback in `resetWebglTextureAtlas`:
`clearTextureAtlas()` is what invalidated the model on the WebGL path, so a
pane without an addon had nothing invalidate it and hit the same skip.
Scope note: the e2e spec guards buffer/geometry convergence across the
hide/reveal boundaries and adds idle-agent and headful desktop-hide cases, but
it cannot observe a stale canvas — both oracles built for that (canvas-vs-buffer
ink sampling, screenshot-vs-forced-repaint) were proven blind by injecting the
defect, and the spec header documents why. The unit tests pin the ordering and
the atlas-preservation invariant. Refs STA-2694.
Co-authored-by: Orca <help@stably.ai>
* docs(terminal): hand off the STA-2694 reveal-artifact investigation
Records both fixed defects with their xterm mechanisms, the reveal/wake call
graph, why every e2e oracle for a stale canvas was proven blind, how to arm the
in-app render-desync sentinel on real hardware, and the one unverified lead
(dimension staleness) that would explain why a window resize specifically is
the repair users find. Refs STA-2694.
Co-authored-by: Orca <help@stably.ai>
* Revert "fix(terminal): clear the render model on the plain-refocus repaint path"
This reverts commit 0f7ec4458d37010338f16e70ff06957cb335e074.
* test(terminal): add a draw-command oracle for reveal repaints, and correct the STA-2694 scope
Every pixel oracle tried for STA-2694 was blind: `drawImage` on a
non-preserveDrawingBuffer WebGL canvas returns a re-rendered copy, and
Playwright's screenshot drives a fresh compositor frame that heals a stale paint
before capture. Reading pixels is self-defeating here — the read triggers the
repaint that hides the bug.
Count the WebGL draw commands instead, by wrapping GlyphRenderer.updateCell and
gl.drawElementsInstanced on the live pane. A draw command cannot be healed after
the fact, so "did the reveal actually repaint?" becomes directly observable.
Teeth-verified: removing releaseAbandonedSynchronizedOutput from
schedulePaneRevealPresent fails the stranded-latch test.
Two findings, both of which change previously-committed claims:
1. The 1s watchdog does NOT bound the synchronized-output defect. It is armed
only inside `bufferRows`, and `refreshRows` returns at its `_isPaused` check
first — so while a pane is occluded nothing reaches `bufferRows` and no timer
is ever pending. A pane hidden mid-`?2026h` holds the latch with no watchdog
behind it, indefinitely. ed1eaf55f1's "closes a bounded window" scope note was
wrong; this is the unbounded garble the report describes, and the fix closes
it. Corrected in the module doc comment.
2. It refutes the diff-based-staleness hypothesis behind 0f7ec4458d (reverted in
8d5eacecb4). `_updateModel` does early-continue per unchanged cell, but
`GlyphRenderer.render` then copies vertices for EVERY row up to
`lineLengths[y]` and issues ONE full-viewport draw — measured identical
instance counts (562) for a diff-skipped and a model-cleared refresh, with
updateCell at 0 vs 561. The DOM renderer likewise replaceChildren()s every
row unconditionally. Clearing the model could not change what reached the
screen, and `_clearModel(true)` zeroes every glyph vertex while
`RenderService.clear()` fires no repaint of its own — so it opened a
blank-viewport window (also asserted here) for no benefit.
Also keeps the idle-agent and headful desktop-hide cases from the reverted
commit, since those were independent of the refuted production change, and
rewrites the alt-screen spec header to point paint questions at this oracle.
Refs STA-2694.
Co-authored-by: Orca <help@stably.ai>
* docs(terminal): rewrite the STA-2694 handoff after the refutation
Records that the garble window is unbounded (the 1s watchdog never arms for an
occluded pane), that the diff-based-staleness hypothesis was refuted by
measurement and reverted, why pixel oracles are structurally blind here, and the
two leads now closed by measurement (dimension staleness, lazy atlas bindings).
Refs STA-2694.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): capture visual proof of the STA-2694 stale paint
The earlier screenshot oracles were blind because they compared a revealed pane
against a repaired one and both ran the same repaint code. Capturing the defect
directly works instead, because the mechanism is self-preserving: while
synchronizedOutput is latched, refreshRows returns before reaching the renderer,
so a compositor frame just re-composites the existing canvas texture and the
stale pixels survive the screenshot rather than being healed by it.
Latch a frame, write a full new frame the pane cannot paint, and capture. The
screenshot comes back byte-identical to the pre-hide one while the buffer holds
the new frame — the buffer/screen divergence users report — and differs after
the reveal repaint runs. Asserts both halves, so it fails if either the defect
stops reproducing or the fix stops repairing it.
Refs STA-2694.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): note where the xterm gate-order double is pinned for real
The unit double encodes RenderService's paused-then-latch gate order, which can
drift on an xterm upgrade. Point at the e2e oracle that pins the same order
against the real renderer, so a future upgrade has a trail to the authoritative
check. Refs STA-2694.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): add a perf budget for the synchronized-output release
releaseAbandonedSynchronizedOutput runs inside resetWebglTextureAtlas, which a
streaming alt-screen TUI can reach through the terminal-output atlas recovery
path — not only on reveal. Measure rather than assert that this costs nothing.
Steady state (a TUI that closes every frame it opens): 200 bracketed frames
produce zero releases, zero extra draw calls, and an unmeasurable early-out
cost. Worst case (every reveal finds a latched frame): 50 latched atlas resets
at 0.08ms each. Both are asserted with headroom, so the guard catches a future
change that makes this scan the buffer per pane rather than flaking on machine
speed. Refs STA-2694.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): address review — drive real code paths, close vacuity gaps
CodeRabbit caught a genuine tautology in the perf budget: it timed a
hand-copied mirror of the early-out rather than the shipped function, so the
assertion would have held even if the real code grew a buffer scan. Driving
resetWebglTextureAtlases instead moved the measured cost from ~0 to ~0.03ms per
call, which is the honest number for the whole recovery; bound re-set to 0.4ms
(10x measured).
Other review fixes:
- Assert the draw counts both perf tests were measuring and logging but never
checking, so the 'no extra draws' titles now mean something.
- Fail fast when decPrivateModes is unavailable; previously the latched test
would pass without ever exercising the fix.
- Re-check the latch right after the worktree switch in the mid-frame test: the
pane is visible until then, so the 1s watchdog can arm and clear it before the
hide, making the run vacuous.
- Count scheduleRevealPresent invocations instead of returning a literal true,
so a missing test hook no longer masquerades as a production failure.
- Assert the latch clears on every reveal iteration, not just the last.
- Make the fixture heartbeat write atomic (tmp + rename); writeFileSync
truncates first, so a reader could see '' and read it as frame 0.
- Relabel assertRevealPixelsNeedNoRepair as the weak secondary check it is; it
contradicted the file header by calling itself 'the decisive paint assertion'.
Refs STA-2694.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(linux): restore Ubuntu 20.04 launch by pinning node-pty glibc symbols (#9902)
The bundled node-pty pty.node is compiled from source in release CI on
ubuntu-latest (glibc 2.39). glibc's 2.32-2.34 libpthread/libutil merge
relocated openpty/forkpty (GLIBC_2.34) and pthread_sigmask (GLIBC_2.32)
into libc under new symbol versions, so the from-source build bound to
versions absent on Ubuntu 20.04 (glibc 2.31). The main process imports
node-pty at startup, so the app crashed on launch. pty.node is the sole
blocker (Electron needs GLIBC_2.25; other native modules <= 2.17).
- Patch node-pty: a .symver shim pins the 3 symbols to their pre-merge
version (GLIBC_2.2.5 x64 / GLIBC_2.17 arm64), and Linux-only ldflags
force libutil.so.1/libpthread.so.0 back into DT_NEEDED. Guarded to
Linux; macOS/Windows untouched.
- Add a packaging gate (verify-linux-glibc-floor.cjs, afterPack): reads
each bundled native binary's objdump -p version needs and fails the
Linux build if any strong GLIBC_/GLIBCXX_/CXXABI_ node exceeds stock
Ubuntu 20.04 (glibc 2.31 / GLIBCXX_3.4.28 / CXXABI_1.3.12). Catches
GLIBC_ABI_DT_RELR, rejects GLIBC_PRIVATE, skips weak needs, fail-closed.
- Docs + tests; the lazy sherpa-onnx speech prebuilt (GLIBCXX_3.4.29,
never loaded at launch) is a documented libstdc++-floor exemption.
* fix(linux): assert DT_NEEDED provider deps in the glibc-floor gate
Harden the packaging gate (flagged in adversarial re-eval): the version-floor
check alone can false-pass if the patch's forced `-l:libutil.so.1` ever silently
drops — the pinned openpty@GLIBC_2.2.5 still resolves from libc's compat alias at
build time, but fails to load on Ubuntu 20.04 where openpty/forkpty live only in
libutil. The gate now also asserts that any binary importing openpty/forkpty
keeps libutil.so.1 in DT_NEEDED. Validated on a real symver-pinned .so with
libutil dropped (now fails) vs. present (passes). Documents the recommended
real-host smoke-test follow-up.
* docs(headless-server): add upgrade SOP for orca serve on Linux
The headless Linux guide covered install/run/systemd but had no upgrade
section, leaving operators to guess how to move to a new AppImage without
losing state.
Add an "Upgrade" section documenting the manual SOP (serve mode never
auto-updates) and one troubleshooting bullet:
- State lives under the service user's ~/.config (orca + Orca dirs),
independent of /opt/orca, and orca-data.json is forward-migrated on load,
so a forward upgrade is safe.
- Replace the binary with an atomic same-filesystem rename (download to
.new, verify, mv) — never curl -o over the FUSE-mounted live binary.
- Back up the whole .config before upgrading, because rollback is NOT
binary-only safe: an older build strips newer orca-data.json fields it
doesn't recognize, and the .bak.* ring is corruption-recovery, not a
pre-upgrade copy.
- Note there is no headless version command; track the release tag instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(headless-server): harden the orca serve upgrade/rollback runbook
Address CodeRabbit review on #9575:
- Fail closed: run the upgrade block under `set -euo pipefail`, remove any stale
`.new` file before download, and gate the atomic `mv` on an explicit ELF check
so a failed/partial/non-ELF download can never be promoted.
- Keep /opt/orca/VERSION tied to the installed binary: a single `TAG` variable
drives both the download URL and the recorded VERSION, saved as VERSION.prev on
upgrade and restored on rollback so the audit file never drifts.
- Crash-loop troubleshooting now points to Roll back first (restores the
pre-upgrade orca-data.json) instead of re-running Upgrade.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(headless): harden server upgrade SOP
---------
Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
badgen.net rate-limits against GitHub and paints "429" into the badge.
Switch stars to shields.io (repo stargazers_count), use a static MIT
license badge, and drop /stargazers links which now 404 for the public
after GitHub's July 2026 stargazer access restrictions.
* Add native chat skill and command picker with host-aware discovery
Adds a unified, keyboard-first skill and command picker to native chat that:
- Uses agent-native invocation syntax (slash for Claude/OpenClaude/Grok, dollar for Codex)
- Discovers skills only on the pane's execution host (local, WSL, SSH-unavailable, or runtime)
- Groups or separates commands and skills per agent configuration
- Deduplicates by canonical path but preserves visibility through all contributing roots
- Handles IME composition, loading states, and errors without claiming PTY-level control
- Records picker telemetry (open, item accepted, send classification, discovery outcomes)
- Extends shared agent profiles to define per-agent skill grammars and source ownership
* Remove obsolete reference and design documentation
Clean up stale design specs, implementation plans, and investigation notes from
docs/reference/. These documents predate the current implementation and are no
longer actively maintained or referenced by the codebase.
* Extract shared skill discovery utilities and add skill invocation envelo
- Move skill comparison and source classification to shared module for native/WSL reuse
- Extract display text sanitization to prevent control/zero-width character spoofing
- Add native-chat command envelope parser and surfacer for skill invocations
- Extend discovery timeout backstop to account for WSL metadata read sequence
* Localize skill picker UI for Spanish, Japanese, Korean, Chinese
Translate skill picker UI strings including commands, skills, loading
states, error messages, and scope labels for the new skill picker feature
across four language locales.
* Fix skill picker bugs and improve code robustness
- Fix i18n plural handling: rename `count` to `sourceCount` to prevent unintended plural-key resolution in localized strings
- Fix skill discovery array mutations: copy `root.providers` to prevent bugs during dedup merge
- Fix image attachments being silently dropped when message text starts with /skill or agent prefix
- Extract `quoteBashString` utility for WSL command code reuse across builders
- Add line-separator safety characters (0x2028/0x2029) to skill display filter
- Remove stale doc reference links and clarify inline comments
* Add reference docs for git compatibility and headless Linux server setup
Track previously untracked operational guides in `docs/reference/` that
explain Git binary compatibility requirements across host types and how to
run `orca serve` on headless Linux. Update AGENTS.md and README.md to link
to these references.