Skip to content

Add Windows parent-identity support + cli_autoupdate_stage telemetry - #895

Open
codyde wants to merge 3 commits into
masterfrom
cody/windows-parent-identity-and-autoupdate-telemetry
Open

codyde wants to merge 3 commits into
masterfrom
cody/windows-parent-identity-and-autoupdate-telemetry

Conversation

@codyde

@codyde codyde commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two fixes the 4.57.3 production data turned up:

  1. Windows parent_identity — process_snapshot() and parent_boot_time() previously had no Windows implementation, so every Windows user fell through to the per-process cli_<base64> fallback. Audit shows ~100% of Windows agent users still hit fallback on 4.57.3.
  2. cli_autoupdate_stage telemetry — the download/stage step of auto-update ran silently in a detached child. We had visibility into apply (TTY-only) but no signal on download success rate, lock contention, or network failures.

Why

Windows fix

Audit 2026-05-11 of 4.57.3 traffic broken down by OS for events still hitting cli_<base64> per-process mint:

Caller OS Arch Fallback events Distinct users
claude_code windows x86_64 155 22
claude_code windows aarch64 32 2
claude_code linux x86_64 4 1

The pattern is unambiguous — Windows users are at ~100% per-process fallback even on the patched release. Root cause:

// Before this PR:
#[cfg(unix)]   { build_unix_snapshot().unwrap_or_default() }
#[cfg(not(unix))] { HashMap::new() }   // Windows: empty map → parent_identity() returns None

#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn parent_boot_time(_pid: u32) -> Option<u64> { None }   // Windows: always None

parent_identity() requires both a non-empty snapshot and a valid parent_boot_time — Windows failed both. The chain unwound:

parent_identity() = None
  → persistent_agent_session_id() = None
    → fall through to per-process cli_<22-char-base64> mint

This PR:

  • Adds build_windows_snapshot() using CreateToolhelp32Snapshot + Process32FirstW/Process32NextW. The command field uses szExeFile (executable basename) rather than full argv — the toolhelp32 API doesn't expose the full command line and pulling it via NtQueryInformationProcess is undocumented kernel API.
  • Adds parent_boot_time() for Windows using OpenProcess + GetProcessTimes. Returns the FILETIME as u64 (100-ns intervals since 1601 UTC). Stable across the process lifetime, not affected by reboots the way Linux clock-tick starttime is.

All required winapi features (tlhelp32, processthreadsapi, handleapi, minwindef, winnt) are already in Cargo.toml.

Auto-update telemetry

Currently we have:

Step Telemetry?
Background API check ❌ silent
try_dispatch_update (routing decision) ❌ silent
spawn_background_download (fork detached child) ❌ silent
background_stage_update (actual download + stage) ❌ silent ← this PR
try_apply_staged success ✅ cli_autoupdate_apply (TTY-only)
Package-manager updates (brew upgrade railway etc.) ❌ silent

Without stage telemetry, we have no visibility into:

  • How often downloads fail (network, checksum, IO)
  • Lock contention rate
  • Latency of the download step

This PR adds cli_autoupdate_stage fired from background_stage_update in the detached child:

outcome success error_message
Staged successfully true null
Lock held by another stage process false "lock_held"
Download / IO / checksum failure false error string (truncated to 256 chars)

Reuses the existing CliTrackEvent / cliEventTrack path so the event flows through the normal telemetry pipeline with caller attribution (claude_code / codex / opencode / etc.) inherited from whatever invocation triggered the update.

Tests

  • filetime_to_u64_combines_high_and_low_halves (Windows-gated): pins the bit-shift encoding so a future refactor can't silently break the persistent-file btime invariant.
  • windows_snapshot_includes_self_and_parent (Windows-gated): integration check that the toolhelp32 snapshot actually returns this process with a non-zero ppid. Catches winapi linkage failures / permission errors at runtime.
  • All 17 existing tests pass unchanged on macOS / Linux.

Test plan

  • cargo build — passes on macOS
  • cargo test --bin railway telemetry — 17/17 passing on macOS
  • cargo fmt --all -- --check — clean
  • cargo clippy --all-targets --all-features — no warnings on touched files
  • CI: Windows test run will exercise the two new Windows-gated tests for the first time
  • Post-release: verify cli_autoupdate_stage events appear in backboard.cli_autoupdate_stage (or wherever PostHog routes them)
  • Post-release: verify the Windows fallback population shrinks toward ~0% over the next 24-48h as users update to 4.57.4

Companion context

Builds directly on #894 (env var name fixes + persistent file fallback). This PR completes that PR's coverage by making the persistent-file path actually work on Windows.

🤖 Generated with Claude Code

# Windows parent-identity (fixes the remaining 4.57.3 fallback gap)

Audit 2026-05-11 of 4.57.3 traffic found 0.75% of claude_code events and
2.6% of codex events still hitting the per-process cli_<base64> mint
instead of the new persistent-file UUID path. The fallback was concentrated
entirely on Windows: 24 distinct Windows claude_code users at 100%
fallback, plus a handful of Linux x86_64 sandboxed cases.

Root cause: process_snapshot() returned an empty HashMap on non-unix
platforms (the existing #[cfg(unix)] gate predated the persistent-file
work), and parent_boot_time() had no Windows implementation. parent_identity()
therefore always returned None on Windows, persistent_agent_session_id()
always returned None, and the precedence chain fell through to the
per-process mint.

This adds:
  - build_windows_snapshot(): uses CreateToolhelp32Snapshot +
    Process32FirstW/NextW to walk the live process table. szExeFile (the
    only command-style field the toolhelp32 API exposes) is the
    executable basename, sufficient for the substring-based caller
    detection consumers.
  - parent_boot_time() on Windows: OpenProcess + GetProcessTimes returns
    creation time as a FILETIME (100-ns intervals since 1601 UTC). Stable
    across the process lifetime, not affected by reboots the way Linux
    clock-tick starttime is.

All winapi features required (tlhelp32, processthreadsapi, handleapi,
minwindef, winnt) are already in Cargo.toml from existing dependencies.

# cli_autoupdate_stage telemetry

The auto-update flow had visibility into only the *apply* step (TTY-only
event from main.rs at try_apply_staged success). The download/stage step,
which runs in a detached child via RAILWAY_STAGE_UPDATE, was silent —
download failure rate, network timeouts, and lock contention were all
unobservable. The 4.57.3 version-adoption curves we can see in the
warehouse are inferred from cli_version on every event, not directly
measured.

This adds a cli_autoupdate_stage event fired from background_stage_update
in the detached child after download_and_stage completes:

  - success=true:  staged successfully (cache stays until apply)
  - success=false, error_message="lock_held": concurrent stage in flight
  - success=false, error_message=<truncated>: real download/checksum/io
    failure

Reuses the existing CliTrackEvent/cliEventTrack path so the event lands
in the warehouse alongside other cli_* events and inherits the caller
attribution (claude_code / codex / opencode etc.) of whatever spawned
the update.

# Tests

Two Windows-gated tests added:
  - filetime_to_u64_combines_high_and_low_halves: pins the bit-shift
    encoding of FILETIME so a future refactor can't silently break the
    persistent-file btime invariant.
  - windows_snapshot_includes_self_and_parent: integration check that the
    toolhelp32 snapshot actually returns this process and has a non-zero
    ppid — catches gross winapi linkage / permission failures.

macOS and Linux paths unchanged; 17/17 existing tests still passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codyde codyde added the release/patch Author patch release label May 11, 2026
codyde and others added 2 commits November 10, 2026 08:40
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-identity-and-autoupdate-telemetry

Resolutions:
- src/telemetry.rs: took master's Windows parent identity (build_windows_snapshot
  + parent_boot_time from #952), which supersedes this PR's version. Dropped the
  filetime_to_u64 test (helper does not exist on master); kept the
  windows_snapshot_includes_self_and_parent test against master's snapshot fn.
- src/main.rs: kept cli_autoupdate_stage telemetry; error truncation now uses
  telemetry::truncate_message (char-boundary safe) instead of a byte slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codyde codyde added release/skip Author no release and removed release/patch Author patch release labels Nov 10, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release/skip Author no release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant