fix(hooks): cut synchronous work from session start/end paths

main

Commit

suhaanthayyil2mo ago

Profiling (perf spans + live timing against a hanging/unreachable API host) traced the "Claude Code startup/exit is noticeably slower" report in #450 to a single hot spot: handleLifecycleSessionStart's trails- enablement cache refresh dialed the network synchronously on the SessionStart hook whenever the hourly cache was unknown/stale (first session in a repo, TTL expiry, or a changed auth key).

Before (10-run medians, session-start hook, DEBUG perf spans):

  • warm cache / fast-failing network: ~35-40ms
  • stale cache + unreachable/blackholed API host: ~1030ms every time (bounded by trailEnablementSessionStartRefreshTimeout=1s, since ResolveDataAPIToken's /.well-known/entire-api.json discovery dial has no loopback/offline fast path)

Stop and SessionEnd (exit) hooks were already fast (~40-50ms) since the refresh only runs on SessionStart.

Fix: the network refresh now runs in a detached subprocess (entire __refresh_trail_enablement), spawned only when the local, network-free cache check finds the value unknown — mirroring the existing detached-analytics pattern in cmd/entire/cli/telemetry. The "forge not supported" case still resolves locally (no network, no spawn). SessionStart itself now only resolves the scope (local git remote + auth-key lookup) and decides whether to spawn; it never blocks on TrailsEnabled.

After (same matrix): session-start stays ~35-40ms regardless of network reachability; the detached refresh still completes (or gives up) on its own bounded timeout, so the cache is still populated once the host responds.

Added a regression test asserting SessionStart never dials a sentinel httptest server synchronously and completes in under 1s even against a deliberately slow host, plus a test that the detached refresh itself is still time-bounded against a host that never responds.

Fixes #450