Files
orca/src/shared/daemon-lifecycle-telemetry.ts
T
fde0fe35a7 fix(daemon): stop giving new terminals to a daemon whose AppImage files are gone (#28011)
* fix(daemon): stop giving new terminals to a daemon whose AppImage files are gone

On Linux AppImage, a terminal daemon can outlive the mount it runs from and
keep answering health checks until it SIGBUSes. Orca now reads the daemon's
/proc exe link and mount table at adoption: an idle severed daemon is
replaced; a busy one is kept for its sessions but quarantined, so fresh
terminals use the local provider and a healthy RPC never re-admits them.
Status and a verified restart are available over RPC and as
`orca terminal-daemon status|restart`.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(daemon): lift the severed quarantine once a new daemon owns the endpoint

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(daemon): a severed-daemon restart counts fallback terminals and never stops a replacement

The restart re-checks the authenticated endpoint owner before any teardown
and again at shutdown, and never falls back to a pid kill when targeted.
Status counts the local fallback terminals the restart also ends.

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(daemon): merge duplicate import

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(runtime): classify the terminalDaemon RPC namespace as host-owned

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 08:50:25 -07:00

43 lines
1.6 KiB
TypeScript

// Enums + bucketing for the `daemon_lifecycle` telemetry event (STA-2376).
// The daemon's own retirement cause (pam-rejections/probe-timeouts) lives in the
// subprocess and never crosses into the app, so every reason here is one the app
// itself decides: a startup replace, or an observed death→respawn.
// Startup launcher replaced a still-connectable daemon (each maps 1:1 to a `daemon-replacement-preflight.ts` decision).
export const DAEMON_REPLACE_REASONS = [
'unhealthy_resolver',
'stale_bundle',
'different_app_path',
'failed_health_check',
'severed_tcc_attribution',
'severed_host_image'
] as const
export type DaemonReplaceReason = (typeof DAEMON_REPLACE_REASONS)[number]
// Adapter observed the daemon die and forked a replacement.
export const DAEMON_RETIRE_REASONS = ['died_respawn'] as const
export type DaemonRetireReason = (typeof DAEMON_RETIRE_REASONS)[number]
export const DAEMON_LIFECYCLE_TRANSITIONS = ['replaced', 'retired'] as const
// Bucketed, never raw: exact live-session counts could fingerprint heavy users. `unknown` when
// the count couldn't be verified (null) — e.g. a wedged daemon or an already-dead respawn target.
export const DAEMON_LIFECYCLE_SESSION_BUCKETS = ['0', '1', '2-5', '6+', 'unknown'] as const
export type DaemonLifecycleSessionBucket = (typeof DAEMON_LIFECYCLE_SESSION_BUCKETS)[number]
export function bucketDaemonLiveSessionCount(count: number | null): DaemonLifecycleSessionBucket {
if (count === null) {
return 'unknown'
}
if (count <= 0) {
return '0'
}
if (count === 1) {
return '1'
}
if (count <= 5) {
return '2-5'
}
return '6+'
}