fix(daemon): emit replaced telemetry on runtime unhealthy_resolver respawn

CodeRabbit: adapter-driven macOS resolver replacements forked a new daemon
without a lifecycle event. Emit trackDaemonReplaced (not retired) so field
diagnosis of #7936 covers the runtime path without mislabeling it as death.
This commit is contained in:
Brennan Benson
2026-07-24 16:42:34 -07:00
parent 7e80d3381d
commit 1e60ca87a4
3 changed files with 11 additions and 3 deletions
+3 -1
View File
@@ -807,11 +807,13 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => {
await replacementAdapter.options.respawn?.('daemon_died')
expect(originalSpawner.resetHandle).toHaveBeenCalledTimes(1)
expect(originalSpawner.ensureRunning).toHaveBeenCalledTimes(1)
// STA-2376: an observed daemon death → respawn emits the retirement lifecycle event.
// STA-2376: death → respawn retires; runtime unhealthy_resolver is a replace (not a false death).
expect(trackDaemonRetiredMock).toHaveBeenCalledWith('died_respawn')
trackDaemonRetiredMock.mockClear()
trackDaemonReplacedMock.mockClear()
await replacementAdapter.options.respawn?.('unhealthy_resolver')
expect(trackDaemonRetiredMock).not.toHaveBeenCalled()
expect(trackDaemonReplacedMock).toHaveBeenCalledWith('unhealthy_resolver', 0)
// Still only one spawner in the whole test — nobody new was constructed.
expect(spawnerInstances).toHaveLength(1)
})
+6
View File
@@ -702,6 +702,9 @@ export async function initDaemonPtyProvider(
if (reason === 'daemon_died') {
console.warn('[daemon] Daemon process died — respawning')
trackDaemonRetired('died_respawn')
} else if (reason === 'unhealthy_resolver') {
// Runtime resolver replace (adapter only fires when live sessions are verified empty). Not a death.
trackDaemonReplaced('unhealthy_resolver', 0)
}
newSpawner.resetHandle()
await newSpawner.ensureRunning()
@@ -887,6 +890,9 @@ async function runRestartDaemon(): Promise<RestartDaemonResult> {
if (reason === 'daemon_died') {
console.warn('[daemon] Daemon process died — respawning')
trackDaemonRetired('died_respawn')
} else if (reason === 'unhealthy_resolver') {
// Runtime resolver replace (adapter only fires when live sessions are verified empty). Not a death.
trackDaemonReplaced('unhealthy_resolver', 0)
}
currentSpawner.resetHandle()
await currentSpawner.ensureRunning()
+2 -2
View File
@@ -9,8 +9,8 @@ import {
} from '../../shared/daemon-lifecycle-telemetry'
import { track } from '../telemetry/client'
// Startup launcher replaced a still-connectable daemon. `versionSkew` = daemon's pid-file appVersion
// differs from the current app (omit when the decision didn't hinge on version, e.g. a different app path).
// Replaced a still-connectable daemon (startup launcher or runtime resolver-health path).
// `versionSkew` = pid-file appVersion differs from current app (omit when not version-driven).
export function trackDaemonReplaced(
reason: DaemonReplaceReason,
liveSessionCount: number | null,