Only assistant records carry usage, but the parser ran JSON.parse on every line
first and checked the type after. Claude transcripts interleave user and
tool-result lines that routinely embed whole files or captured command output,
so the scanner built and discarded a full object graph for each of them.
A substring gate on the line rejects them first: median 50.5ms to 8.4ms over
20000 realistic lines.
* fix: satisfy GitLab hook and test lint gates
* Rename electron-vite target config to .cts
The .cts extension keeps the config as CommonJS, allowing electron-vite
to load each parallel target without sharing its timestamp-named ESM
temp file.
* fix(ssh): fence stale kills and retired pane replay
* fix(ssh): support cancellable interactive authentication
* fix(ssh): await remote catalog before snapshot adoption
* fix(pty): contain Windows ConPTY input failures
* fix(power): avoid redundant macOS display blocking
* perf(editor): narrow markdown override subscriptions
* fix(quick-open): close directory handles after reads
* refactor(linux): remove unused proc socket scanner
* fix(usage): apply flat Sonnet 4.6 pricing
* ci: prime Node next native test cache
* docs(skills): resolve snapshot cleanup data path
* fix(ssh): recover install locks after host reboot
* test(ssh): recognize boot-aware install locks
* test(ssh): prove previous-boot lock recovery live
* test(wire): pin pre-metadata release coverage
* fix(terminal): preserve remote tab ownership through recovery races
* test(runtime): fence replaced terminal handles in agent guard
* fix(ssh): preserve remote snapshot authority across polls
* fix(pty): contain late ConPTY output EPIPE
* test(pty): register Windows exit watcher before kill
* fix: close SSH and tab readiness race gaps
* fix(tabs): retain headless order and placeholder titles
* fix(build): avoid parallel electron-vite config race
* test(windows): avoid MSYS temp path rewriting
* test(windows): avoid killing exited PTY
* fix(pty): avoid late ConPTY input teardown race
* fix(terminal): sync reconnect error ownership after commit
* fix(runtime): use canonical worktree identity comparison
* test(ssh): assert complete cold-hydration baseline
* test(windows): invoke quoted retention fixture via PowerShell
* test(windows): read ConPTY grid through mode con
* fix(terminal): publish PTY replacements atomically
* fix(terminal): infer stale identity on reattach
* fix(terminal): fence stale pane PTY callbacks
* fix(terminal): fence stale pane binds after rebind
* fix(terminal): reject stale pane transport callbacks
* fix(terminal): fence mirrored reattach spawn callbacks
* fix(terminal): replace stale pane PTYs on remount
* fix(ci): size the Windows launcher-compile test budget from measurement
`native-smoke (windows-latest)` fails ~4.5% of runs on
`preserves a multiline argument through the compiled remote launcher`
with "Test timed out in 15000ms" — on unrelated PRs, for reasons that
have nothing to do with them. Across 176 sampled attempts it is the only
red that job produced, and it hit seven different PRs in two days:
#16900, #16904, #16915, #16955 (twice), #16979, #17014, #17085.
The test is six process creations: powershell.exe forks csc.exe, then
the freshly compiled orca.exe forks node.exe, twice. Hosted Windows
runners periodically slow process creation down, and this test amplifies
that far harder than anything else in the job. Comparing the 80 attempts
where it ran under 3s against the 12 where it ran over 12s, its own
median goes 2198ms -> 15917ms (7.2x) while the same file's
powershell-only test moves 556 -> 686ms (1.2x), the cmd.exe and Git Bash
process tests in the neighbouring file move 1.4x, and the other 35 files
put together move 1.5x.
Measured across those 176 attempts: 1881ms to 35438ms, p50 4264ms,
correlation +0.881 with the job's total Vitest duration. 8 of 176 (4.5%)
exceeded the 15s cap; 2 of 176 (1.1%) also exceeded the shared 30s
testTimeout, so deleting the override and inheriting the config is not
enough on its own. 60s clears all 176 with 1.7x headroom on the worst.
This is slow, not hung. Every body here is synchronous spawnSync, so
Vitest cannot interrupt one — the timer fires only after the body
returns and the reported duration is real elapsed time. That is why a
failure reads `× ... 22464ms` under `Test timed out in 15000ms`. The
work finished; the stopwatch was short. Seven reruns at one identical
head measured 2053 / 4680 / 5551 / 8732 / 13506 / 14868 / 21937ms — the
last of those would have been red on code that had not changed.
The 15s came from #8897, which raised this test off Vitest's built-in 5s
default because the job then ran bare `pnpm vitest run`. #8909 landed
3h27m later and pointed the job at config/vitest.config.ts, which is the
real fix for that. The constant stayed behind and has been the binding
budget ever since.
* fix(terminal): fence stale remount reattach ownership
* fix(terminal): reconcile mounted pane identity after replacement
* fix(terminal): fence stale reattach fallback ownership
* fix(terminal): fence deferred SSH reattach ownership
* fix(terminal): fence stale split pane ownership callbacks
* fix(terminal): keep stale spawns from consuming startup
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
The three usage scanners and their stores, plus the renderer usage-overview
model, each carried a file-level `eslint-disable max-lines` and had grown to
338-769 counted lines against a 300-line budget. AGENTS.md calls for splitting
rather than suppressing, and config/max-lines-baseline.txt is a shrink-only
ratchet, so this removes all seven suppressions and prunes their entries
(341 -> 334).
Each file is cut along the seams it already had -- and that several of the
suppression comments named out loud: filesystem discovery / record parsing /
attribution / aggregation for the scanners, and pricing policy / scope filters /
rollups / session rows / automation attribution for the stores.
Pure move, no behavior change. Code is relocated verbatim; the only edits are
import plumbing and, where a private class method became a free function, the
mechanical `this.state` -> `state` parameter threading. Every converted call
site passes `this.state` at call time and the automation path takes a live
`getState: () => this.state` getter, so no state is snapshotted. No barrel
exports: each new module owns real logic and importers point at the owner.
Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, each green
when re-run serially), no import cycles among the 64 affected modules, and a
statement-level diff of every split confirms the moves are verbatim.
* refactor(usage): share the session/daily fold between Codex and OpenCode
The Codex and OpenCode scanners each carried their own byte-identical copy of
the ~325-line aggregation pipeline (createEmptySession, the three breakdown
folds, finalizeSessions, mergeSessions, mergeDailyAggregates). Two copies means
a token-accounting fix — a bucket that double-counts, a merge that drops a
breakdown row — lands in one provider and silently not the other. The copies had
already started to drift in comments only; the next drift would have been in
arithmetic.
The providers differ in exactly one dimension: the extra metric folded alongside
the token counters (Codex `hasInferredPricing`, OpenCode `estimatedCostUsd`).
That is now injected as an empty/fromEvent/fold triple, so the shared code stays
generic without collapsing the two record schemas into a nullable union. The
clone strategy stays per-provider (`cloneSessionForMerge` vs `structuredClone`)
rather than being unified on the assumption that the difference is accidental.
`usage-provider-contract.ts` is the seam a plugin-contributed usage source will
implement. It is deliberately generic over each provider's record types: Claude
bills per turn while Codex/OpenCode bill per event, and `cachedInput` is a subset
of `input` for the latter but a peer bucket for Claude, so a single normalized
record would push nullable handling onto every consumer.
No behavior change. Emitted objects are byte-identical, including key insertion
order — verified by diffing JSON.stringify of the scan output before and after
across mixed models, mixed locations, an inferred-pricing flip, and null vs
non-null cost. Persisted field names and schemaVersion are untouched, so caches
do not invalidate.
* refactor(usage): make the provider contract load-bearing and dedupe worktree refs
Follow-up to the aggregation extraction, addressing three review points.
`UsageProvider`/`UsageScanResult` were declaration-only, which is the same
speculative-interface problem #12077 just deleted 8,900 lines of. They are now
implemented by both real providers via `satisfies`, so the seam is typechecked
against actual scan functions rather than asserted. The blocker was that codex
returns `processedFiles` and opencode returns `processedDatabases`; rather than
rename persisted-adjacent fields, the source key is a type parameter, so each
provider keeps its own on-disk name and the contract still binds. Verified the
constraint bites: swapping the key to 'processedSources' fails typecheck.
`schemaVersion` is part of provider identity in the contract, so each provider's
SCHEMA_VERSION constant (with its cache-invalidation rationale) moves into the
provider module and the store imports it. Values are unchanged (codex 5,
opencode 2) and the stores compare them exactly as before, so no cache
invalidates. This also keeps store -> provider -> scanner acyclic.
`UsageWorktreeRef` collided with the existing export in usage-worktree-metadata
(3 fields, no repoId). Two different exported types under one name in src/main
is worse than the duplication being removed, so the scan-input type is now
`UsageScanWorktreeRef`; usage-worktree-metadata is untouched.
`createWorktreeRefs` was triplicated. Codex, OpenCode, and Claude copies are
byte-identical apart from the return type name (verified by diff), and all three
ref types have the same four fields, so one shared copy replaces all three. This
is the only change to claude-usage/.
No behavior change: same functions, same arguments, same call order. The store
tests' `./scanner` mock still intercepts scanning because the provider captures
the mocked binding; their now-inert `createWorktreeRefs` mock key is dropped so
it does not read as still mocking something.
* chore(dead-code): drop 2k lines of unreachable exports and orphan modules
Ran knip across every build entry (main, preload, renderer, popout, web,
cli, relay, workers, forked sidecars, config scripts) and removed what no
entry graph can reach.
- 11 orphan modules nothing imported, plus one test that only covered them
- 159 unused exports/types, with their now-dead helpers, imports and tests
Each candidate was verified against dynamic references before deletion.
42 knip hits were false positives and are kept: shared modules consumed by
the mobile/ workspace, the src/shared/plugins/** public API, vendored
shadcn primitives, and relay wire-protocol constants held for compatibility.
Adds knip.json + `pnpm audit:dead-code` so this stays measurable.
Verified: pnpm typecheck, pnpm lint, and 2081 tests across the 73 affected
test files all pass.
* chore(dead-code): move knip config under config/
Root-level additions are blocked by the root directory guard.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(worktrees): prevent deletion from blocking Orca
* test(worktrees): loosen async history-delete event-loop bound for CI
The main-thread safety check failed on a loaded runner when a single
timer gap hit ~48ms under the prior 30ms threshold. Keep the bound well
below a recursive sync-rm stall without treating CI jitter as a block.
* test(worktrees): measure history-delete critical path, not timer gaps
setInterval gaps during async rm of thousands of files still flake under
CI scheduling. deleteWorktreeHistoryDir is sync and must only rename, so
assert that critical-path wall time stays well below a recursive walk.
* fix(worktrees): prevent deletion from blocking Orca
Add timeout-based draining of watcher closes so SSH round-trip delays don't
indefinitely block the worktree removal path. Also: order durable temp-file
sweeps ahead of writes to reclaim orphans before accumulation, skip own-process
temps to avoid deleting live writes, swallow persistence errors so disk failures
don't cascade to query callers, and measure history-deletion progress by loop
turns rather than timer gaps to detect blocking on CI runners.
* fix(worktrees): prevent deletion from blocking Orca
Worktree deletion can now proceed even if filesystem watchers or history cleanup operations hang, preventing Orca from freezing. Changes:
- Fence install slots with tokens instead of counters so removals can abandon wedged installs without corrupting later removals
- Timeout-bound watcher unsubscribe operations with a shared drain budget
- Move JSON serialization of large usage caches from queue-time to write-time to avoid blocking main thread
- Async tombstone + schedule history tree deletion instead of blocking recursive rmSync during GC, preventing main-thread stalls ~10s after startup
* Extract usage cache writer into reusable durable snapshot class
Consolidates serialized durable-write and generation-veto logic from
three usage stores into UsageCacheSnapshotWriter. Eliminates duplication,
centralizes multi-MB JSON serialization on the main thread via write-queue
serialization, and vetoes superseded snapshots to avoid wasted rewrites.
* fix(worktrees): prevent deletion from blocking Orca
Worktree deletion used to recursively delete large session trees (hundreds
of MB) on the critical path, stalling the event loop. Instead, rename trees
into a `.pending-delete` tombstone queue and reclaim them asynchronously
off the removal's critical path.
Extracted host tree removal into a reusable helper (`removeHostTree`) that
centralizes Windows retry logic. Added usage-cache flush on quit to prevent
data loss when scans complete right before shutdown. Improved watcher
removal deadline management with reserved tail slices for the final
unsubscribe, and added retry logic for tombstone removals that fail once
under transient Windows locking.
* fix(history): retry failed session tree removals
Tombstoned session trees whose removal fails transiently (e.g., EBUSY
under Windows AV) are now re-queued in-process with bounded exponential
backoff instead of sitting until the next HistoryManager construction.
Prevents a single stuck tree from blocking the entire Orca process.
* feat(usage): price Claude 5 family and GPT-5.6 token usage
Claude Opus 5, Sonnet 5, Fable 5 and Codex gpt-5.6 sol/terra/luna were
absent from the usage pricing tables, so their turns aggregated tokens
but reported no estimated cost.
Rates from Anthropic and OpenAI published pricing. Sonnet 5 gets no
long-context tier: Claude 4.6 and later bill the full 1M window flat.
Sonnet 5 uses the standard $3/$15 rate, not the $2/$10 introductory rate
that runs through 2026-08-31 — the table has no date dimension.
* fix(usage): price the bare gpt-5.6 alias and assert Opus 4.5 separately
OpenAI routes the bare `gpt-5.6` alias to Sol, but only the explicit
`-sol` / `-terra` / `-luna` IDs resolved, so alias-recorded sessions still
reported no cost. Match it exactly rather than by prefix so it cannot
swallow the tier IDs or a future cheaper variant.
Also split the Claude 5 shadowing guard into per-model breakdown
assertions and add the missing Opus 4.5 fixture the test name claimed.
* docs(usage): note Sonnet 5 uses standard, not introductory, rates
* fix: dedupe fork-copied usage history across Claude, Codex, and OpenCode scanners
Coding-agent CLIs copy transcript/rollout history into new files on
resume/fork, and the usage scanners deduped per-file only (or not at
all), so copied history was re-counted once per descendant file
(issue #8006: 38.6B tokens / $72,981 reported vs ~2.3B real).
- claude-usage: cross-file turn ownership keyed on message.id:requestId;
per-file ownedDedupeKeys persisted; deterministic sorted-path claim
order; schema v3 -> v4 so inflated caches rebuild.
- codex-usage: cross-file token_count event ownership keyed on the raw
record identity (sessionId + timestamp + token tuples); fixes both the
copied-prefix re-count and the total-only branch that re-counted the
entire cumulative session per descendant rollout; legacy
.orca-session-copies skip-bytes bridge unchanged; schema v3 -> v4.
- opencode-usage: each sessionId is counted from exactly one database;
the canonical opencode.db claims ahead of stale sibling copies
(opencode-backup.db etc.) so backups no longer double totals, while
backup-only sessions are still counted; schema v1 -> v2.
Regression tests cover fork-copied files counted once (including the
Codex total-only variant), duplicated OpenCode databases, and dedupe
stability across cached incremental rescans.
Co-authored-by: Orca <help@stably.ai>
* Fix cross-tool usage double-counting for fork/resume-copied history
- Widen Claude dedupe keys with message-id and uuid fallbacks so forks
missing requestId still dedupe correctly, and drop Codex's sessionId
from event keys since fork/resume rewrites session_meta.id while
copying identical token_count records.
- Track hasDeferredClaims per cached file/database across Claude, Codex,
and OpenCode scanners so that when an owning file is deleted, only
files that deferred a claim need reparsing to reclaim those turns
instead of rescanning the entire corpus.
- Let OpenCode's live opencode.db reclaim sessions from a stale backup
claim once it reappears, avoiding a frozen stale snapshot.
- Bump schema versions to invalidate caches built with the old,
narrower ownership keys (#8006, #8013 follow-up).
---------
Co-authored-by: Orca <help@stably.ai>
110 files carried an eslint/oxlint-disable max-lines directive but are
already under the default max-lines budget (300 .ts / 400 .tsx / 600 .mjs
/ 800 test), so the suppression is dead. Removing it restores real
max-lines coverage on these files with zero behavior change.
Each removed directive had max-lines as its only rule; verified via a
full oxlint run (0 max-lines violations, 0 new errors). Diff is pure
deletions (200 lines, 0 additions) — no code touched.
Co-authored-by: Orca <help@stably.ai>
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules
Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.
Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):
error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse (19: copy-then-reverse -> toReversed)
warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type (aliasing footgun guard)
- typescript/no-unsafe-function-type (bans bare Function type)
- unicorn/prefer-array-flat-map (map().flat() -> flatMap())
- unicorn/prefer-regexp-test (.match() in bool ctx -> .test())
mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.
Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.
* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse
mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).
Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix: write stats file in chunks to avoid Electron UTF-8 abort
orca-stats.json gains an event on every agent start/stop. After about a
month of use mine had grown to ~3.6k events / ~608 KB, and the app started
hard-crashing a few seconds after every launch (SIGTRAP, no catchable JS
stack):
Assertion failed: (length + 1) <= (capacity())
node::MaybeStackBuffer<char>::SetLengthAndZeroTerminate <- node::Utf8Value
The crash is in StatsCollector.writeToDiskSync(), which saves the whole
file in one writeFileSync(JSON.stringify(data)). Electron 42.3.2's bundled
Node aborts when encoding a string that large to UTF-8 in a single write;
stock Node 24 handles the same file fine and the data is well-formed, so
it's an Electron/Node encoding limit, not bad data. The save runs on a
debounce after agent_start, which restored agents fire on launch -- so it
crashed right after opening.
Write the JSON in 64 KB slices through one fd instead (never splitting a
surrogate pair), and lower MAX_EVENTS 10k -> 1k so the file can't grow back
this large. Lifetime aggregates are unaffected.
Verified by reproducing the abort standalone with the real 608 KB file
under ELECTRON_RUN_AS_NODE, confirming the chunked writer round-trips it
byte-for-byte with no crash, and running a patched build that loads the
file without crashing. The underlying encode abort is an Electron/Node bug
to report upstream.
* fix: harden stats JSON writes
* fix: chunk app state UTF-8 writes
* fix: stabilize status and terminal polling
---------
Co-authored-by: thiagomsoares <5190162+thiagomsoares@users.noreply.github.com>
Add legacy version-first ID branches (3-5-sonnet, 3-5-haiku) in normalizeModelForPricing so legacy logs map to existing pricing entries instead of returning null.
Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>