Repository navigation
fix(orcad): identify instances by kernel start ticks, not drifting wall-clock start time - #28125
Merged
Merged
Conversation
nwparker
force-pushed
the
nwparker/fix-orcad-instance-identity-drift
branch
from
October 13, 2026 00:57
d042e3b to
7db0a8e
Compare
…ll-clock start time On Linux the start time is derived from /proc/stat btime, which drifts (WSL2, NTP), so a live orcad was read as a reused PID and its managed stop reported exited without ever writing the request. The lock and stop request now carry optional linuxStartTicks + bootId; readers prefer them, and a wall-clock mismatch on Linux for records without them is unverifiable, never proof of exit. Co-Authored-By: Claude <noreply@anthropic.com>
…nt drops A request recovered by an older client loses linuxStartTicks/bootId, which made the newer host reject its own receipts and decision records as mismatched. Also exercise the incarnation reader against a real Linux /proc.
nwparker
force-pushed
the
nwparker/fix-orcad-instance-identity-drift
branch
from
October 13, 2026 01:04
7db0a8e to
7fa0541
Compare
nwparker
marked this pull request as ready for review
October 13, 2026 01:20
4 tasks
karlorz
added a commit
to karlorz/orca
that referenced
this pull request
Oct 13, 2026
* Update README downloads badge * fix(i18n): drop chat-mode keys removed from en.json in stablyai#28113 (stablyai#28131) * Speed up Mobile Checks by overlapping typechecks and tests (stablyai#28127) Overlap mobile typechecks with tests and avoid repeated filesystem probes and unused device substitutes in recording replays. Keep state isolated between loaders, give exhaustive snapshot cases an explicit execution deadline, and retire obsolete locale keys that blocked repository validation. * fix(orcad): identify instances by kernel start ticks, not drifting wall-clock start time (stablyai#28125) * fix(orcad): identify instances by kernel start ticks, not drifting wall-clock start time On Linux the start time is derived from /proc/stat btime, which drifts (WSL2, NTP), so a live orcad was read as a reused PID and its managed stop reported exited without ever writing the request. The lock and stop request now carry optional linuxStartTicks + bootId; readers prefer them, and a wall-clock mismatch on Linux for records without them is unverifiable, never proof of exit. Co-Authored-By: Claude <noreply@anthropic.com> * fix(orcad): match stop receipts without the incarnation an older client drops A request recovered by an older client loses linuxStartTicks/bootId, which made the newer host reject its own receipts and decision records as mismatched. Also exercise the incarnation reader against a real Linux /proc. --------- Co-authored-by: Claude <noreply@anthropic.com> * docs(readme): point the Android APK links at 0.0.55 (stablyai#28134) Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * perf(runtime): stop re-sending the worktree list to mobile pollers on every terminal chunk (stablyai#28062) * perf(runtime): let worktree.ps pollers opt out of per-chunk activity churn worktree.ps carries `preview` and an exact `lastOutputAt` on every row, both of which move on each PTY chunk, so the content-addressed snapshot id changed on every poll while any agent printed and the whole catalog was resent. Add an optional `coarseActivity: true` param. When set, the host drops `preview` and floors `lastOutputAt` to 30 s before hashing, so the id holds while only chunk-level activity moves. Callers without the param get the byte-identical response they get today; older hosts strip the unknown key. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * perf(mobile): stop re-sending the worktree list on every terminal chunk The host-screen snapshot poll now asks worktree.ps for `coarseActivity`, so the catalog's snapshot id survives terminal output and the 3 s poll gets a tiny `unchanged` reply instead of the whole catalog. `preview` is optional on mobile rows because a coarse host leaves it out; old hosts ignore the param and keep sending it. Re-records the three worktree.catalog-snapshot goldens; the only semantic change is the new request param. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * perf(runtime): coarsen worktree.ps activity for every snapshot poller Drop the `coarseActivity` param: the only caller that sends `afterSnapshotId` is the mobile snapshot poll, which never renders `preview` and uses `lastOutputAt` only to rank rows. Snapshot polls now always get a blank `preview` and a 30 s-floored `lastOutputAt`, so already-shipped apps stop re-downloading the catalog on every terminal chunk with only a desktop update. Callers without `afterSnapshotId` still get the byte-identical response. Reverts the mobile param, its scenario steps and the three re-recorded goldens, and removes the unread `preview` field from mobile row types. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(runtime): rank coarsened worktree.ps rows so in-bucket swaps keep the snapshot id Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test: drop preview from row fixtures and keep the runtime cast on one line Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): load markdown tabs from any desktop split group; center load-error Retry (stablyai#28132) * fix(mobile): load markdown tabs from any desktop split group and center load-error Retry The desktop answered markdown.readTab only for tabs in its focused split group, so a phone opening a markdown tab from another group got tab_not_found and showed "Couldn't load markdown". Resolve the tab across all of the worktree's groups. Load-error states now share one component that centers the message and its Retry button; the markdown reader's button was pinned to the left. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(renderer): mobile markdown read picks the requested tab across groups Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(renderer): resolve mobile markdown tab to one file id Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(wsl): run WSL git with Windows Git line-ending settings on checkouts Windows Git wrote (stablyai#27778) * fix(wsl): run WSL git with Windows Git settings on Windows-authored drive checkouts * fix(wsl): detect Windows Git checkouts from host autocrlf plus a whole-tree sample * fix(wsl): keep WSL-created worktrees on Linux Git defaults and never commit CRLF on an unproven checkout - worktree add / clone write a new checkout whose .git points at /mnt/..., which Windows Git cannot open; injecting autocrlf=true there produced CRLF files later read unflagged. - Writes on a host-autocrlf=true checkout whose sample is unproven run with autocrlf=input, so staging normalizes CRLF without ever writing CRLF to disk. - Writes wait for a stale host answer and re-check checkout identity after joining a sample. * fix(wsl): compare a joined sample with the checkout identity read after it settles * fix(runtime): stop a project's terminals before the host forgets the project (stablyai#28024) * fix(runtime): stop a project's terminals before the host forgets the project Removing a project on a runtime host (an SSH-managed or paired server) sent repo.rm first; the host resolves terminal.stop through the project's workspace rows, which repo.rm deletes, so the stop failed and the shells kept running on the host. Terminals stop first now. The removal toast also drops the IPC wrapper from its message. * fix(runtime): only stop host terminals when the host will accept the project removal Probe with repo.show (same resolver as repo.rm) so a removal the host rejects, such as an ambiguous project id, no longer kills the shells first. Also poll through terminal_not_writable in the paired-client e2e. * test(e2e): read the host shell pid through sh so non-POSIX login shells work * fix: preserve relay pane claims across managed tab mirrors * fix(mobile): cancel bridge-post retries when a terminal document stops Mobile Checks verify failed on an unhandled ReferenceError: window is not defined inside postToReactNativeWebView's scheduled send() retry while viewport-frame.test.ts ran. Retries lived outside the document lifecycle, so a stop left timers that later read a torn-down window. Bind the default postToHost per scope, clear its timers on stop, no-op posts after stop, and guard window before reading ReactNativeWebView. Preserve bounded retry and deliver-when-bridge-appears behavior. * fix(mobile): use supported own-property check for host overrides * fix(mobile): align bridge retry teardown with document lifecycle contract * style(mobile): format bridge retry lifecycle regression test --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
registered2nd
pushed a commit
to registered2nd/orca
that referenced
this pull request
Oct 14, 2026
…ll-clock start time (stablyai#28125) * fix(orcad): identify instances by kernel start ticks, not drifting wall-clock start time On Linux the start time is derived from /proc/stat btime, which drifts (WSL2, NTP), so a live orcad was read as a reused PID and its managed stop reported exited without ever writing the request. The lock and stop request now carry optional linuxStartTicks + bootId; readers prefer them, and a wall-clock mismatch on Linux for records without them is unverifiable, never proof of exit. Co-Authored-By: Claude <noreply@anthropic.com> * fix(orcad): match stop receipts without the incarnation an older client drops A request recovered by an older client loses linuxStartTicks/bootId, which made the newer host reject its own receipts and decision records as mismatched. Also exercise the incarnation reader against a real Linux /proc. --------- Co-authored-by: Claude <noreply@anthropic.com> (cherry picked from commit bcad291)
Merged
8 of 9 tasks
nwparker
added a commit
that referenced
this pull request
Oct 16, 2026
…, and explain a possibly reused PID Records written before #28125 carry no start ticks or boot id, so on Linux a wall-clock mismatch is unproven and a reused PID kept the lock held forever. A PID owned by another non-root uid cannot be the holder (orcad never changes uid), so that now proves reuse in both lock reclaim and managed stop. When the verdict stays unproven, the error names the live process and the orcad.lock path that is safe to remove once no orcad is running.
nwparker
added a commit
that referenced
this pull request
Oct 16, 2026
…a possibly reused PID (#29548) * fix(orcad): reclaim a pre-#28125 lock whose PID another user now owns, and explain a possibly reused PID Records written before #28125 carry no start ticks or boot id, so on Linux a wall-clock mismatch is unproven and a reused PID kept the lock held forever. A PID owned by another non-root uid cannot be the holder (orcad never changes uid), so that now proves reuse in both lock reclaim and managed stop. When the verdict stays unproven, the error names the live process and the orcad.lock path that is safe to remove once no orcad is running. * fix(orcad): treat the overflow uid as unproven and word the reused-PID error for any boot-less record * test(orcad): keep the Linux-platform lock cases off Windows, where the platform hook also drives data-root privacy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ELI5
When Orca stops its background server (orcad), it first checks that the process ID still belongs to that same orcad, and not to some other program that got the reused ID. On Linux that check compared start times worked out from a boot timestamp that can shift while the machine runs (WSL2 especially). Once that timestamp shifted, Orca decided the orcad "had already exited", skipped asking it to stop, and left it running. Now Linux uses the kernel's own start counter and boot ID, which never shift.
What Changed
Before: on a WSL2 smoke,
/proc/statbtime moved by 47 s in ten minutes. orcad's computed start time then differed from thestartedAtMsinorcad.lockby 110 s and later 157 s, well over the 1.5 s tolerance.completeOrcadManagedStopread that as "PID reused, so exited". It never wrote the stop request, the client deactivated the record anyway, the receipt saidretirement: "unverifiable", and orcad plus its terminal daemon kept running until they exited on their own after 15 minutes of idle.After:
orcad.lockrecords two new optional fields on Linux:linuxStartTicks(field 22 of/proc/<pid>/stat) andbootId(/proc/sys/kernel/random/boot_id).startedAtMsis still written. This is the same drift-free identity the terminal daemon's pid file already uses.compareOrcadProcessIdentity, answersmatches/differs/unknown/unproven:differs, which counts as proof. If the ticks can't be read, the answer isunknown, not proof.unproven, because that time comes from a btime that drifts. On Windows and macOS the OS stores the creation time, so a mismatch there still counts as proof of reuse, as before.differsmeansexited,unprovenmeansunverifiable, andunknownkeeps polling aslive. A death verdict now needs a missing PID or a proven kernel-identity change. The expected ticks come from the request if it has them, otherwise from the lock, but only while the lock still names this instance by pid, start time and nonce.differslets a new orcad reclaim a live holder's lock. Before this, btime drift could also let a second orcad take the lock from a live one.linuxStartTicks/bootIdfrom the remote lock into the stop request when both are there.Why
Ticks plus boot ID is already the identity the terminal daemon trusts (
daemon-incarnation-evidence.ts), and it involves no clock. Alternatives I rejected:This also affects SSH hosts that run on VMs whose clock gets stepped or slewed, not only WSL.
Compatibility
Clients and hosts update independently. Every new field is optional, and a malformed value is salvaged to absent rather than making the lock or request unreadable.
z.objectdrops the two keys, andstartedAtMsis still present. Covered by a test using the older reader shape.unverifiable, neverexited.transactionId.Instance matching between the running orcad and a request (
sameOrcadManagedStopInstance: pid + startedAtMs + nonce) is unchanged, so no request that was accepted before is refused now.Linked Issue
None found.
Visual Proof
N/A. This is host-side process-identity logic with no UI change.
Testing
orcad-managed-stop-identity.test.tscovers:unverifiable, no receiptorcad-instance-lock.test.tscovers:orcad-process-start-time.test.tstests the comparison verdicts directly.Ran
pnpm tc, thesrc/main/orcad+src/main/ssh+src/main/wslsuites (3577 passed), oxlint,check:code-quality:changed,check:max-lines-ratchet,check:reliability-gates,check:ts-nocheck-ratchet,audit:code-quality:type-aware, andpnpm format. All ran on macOS. The real/procreads only run on Linux CI, and this was not re-run on a real WSL2 host.I manually tested these changes locally
Automated tests added/updated, or explained why not below
AI Disclosure
Review
Agent skill upstream boundary
Notes
Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)