fix(agent-awake): warn once while the Linux lid sleep assertion keeps failing (#24839)

* fix(agent-awake): back off and warn once when the Linux lid sleep assertion keeps failing

On a headless Linux host, systemd-inhibit starts and then exits with code 1.
start() reset the failure streak right after the spawn, so every 30 s retry
logged a fresh warning and the retry never backed off.

Keep the streak until an intentional stop or until an inhibitor has held for
a full retry interval, and double the retry delay on consecutive failures
(30 s up to 30 min).

Fixes #20739

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

* fix(agent-awake): keep the lid assertion failure streak across idle/active cycles

stop() reset the warn-once dedupe and backoff, so a host where logind denies
the inhibitor still warned once per agent turn. Reset only after an inhibitor
was actually held for a retry interval.

* fix(agent-awake): keep the 30s lid assertion retry instead of backing off

Backoff delayed re-acquiring the lid inhibitor on laptops after transient
failures; warning once per failure streak already fixes the log flood.

---------

Co-authored-by: brynnclaw <261708852+brynnclaw@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
Brynn Bendixen
2026-10-25 23:56:31 -07:00
committed by GitHub
co-authored by brynnclaw Claude Opus 5.5 OrcaWin
parent de93f326ea
commit 0cbb973440
2 changed files with 111 additions and 23 deletions
+95 -18
View File
@@ -315,26 +315,103 @@ describe('LinuxLidSleepAssertion', () => {
vi.useRealTimers()
})
it('logs repeated identical failures at debug until reset', () => {
const logger = createLogger()
const spawn = vi.fn(() => {
throw new Error('dbus unavailable')
})
let now = 1_000
const assertion = new LinuxLidSleepAssertion({
logger,
now: () => now,
platform: 'linux',
spawn
describe('when systemd-inhibit starts but exits immediately (no usable logind)', () => {
function createRetryingAssertion() {
const logger = createLogger()
const children: FakeSystemdInhibitProcess[] = []
const spawnTimes: number[] = []
const spawn = vi.fn(() => {
const child = new FakeSystemdInhibitProcess()
children.push(child)
spawnTimes.push(Date.now())
return child
})
const assertion: LinuxLidSleepAssertion = new LinuxLidSleepAssertion({
logger,
now: () => Date.now(),
// Mirrors AgentAwakeService: every failure or retry tick re-runs start().
onUnexpectedFailure: (reason) => assertion.start(reason),
platform: 'linux',
spawn
})
const failLatestChild = (): void => {
const child = children.at(-1)
child?.emit('exit', 1, null)
child?.emit('close', 1, null)
}
const spawnGaps = (): number[] =>
spawnTimes.slice(1).map((time, index) => time - spawnTimes[index])
return { assertion, failLatestChild, logger, spawn, spawnGaps }
}
it('warns once while it keeps retrying every interval', () => {
vi.useFakeTimers()
try {
const { assertion, failLatestChild, logger, spawnGaps } = createRetryingAssertion()
assertion.start('status-change')
for (let attempt = 0; attempt < 8; attempt++) {
failLatestChild()
vi.advanceTimersToNextTimer()
}
expect(spawnGaps()).toEqual(Array(8).fill(LINUX_LID_SLEEP_ASSERTION_RETRY_MS))
expect(logger.warn).toHaveBeenCalledTimes(1)
expect(logger.debug).toHaveBeenCalledTimes(7)
assertion.dispose()
} finally {
vi.useRealTimers()
}
})
assertion.start('status-change')
now += LINUX_LID_SLEEP_ASSERTION_RETRY_MS + 1
assertion.start('power-resume')
assertion.stop('settings-change')
assertion.start('status-change')
it('stays quiet across idle/active cycles until an inhibitor is held', () => {
vi.useFakeTimers()
try {
const { assertion, failLatestChild, logger, spawn } = createRetryingAssertion()
expect(logger.warn).toHaveBeenCalledTimes(2)
expect(logger.debug).toHaveBeenCalledTimes(1)
assertion.start('status-change')
for (let attempt = 0; attempt < 4; attempt++) {
failLatestChild()
vi.advanceTimersToNextTimer()
}
assertion.stop('status-change')
assertion.start('status-change')
failLatestChild()
expect(spawn).toHaveBeenCalledTimes(6)
expect(logger.warn).toHaveBeenCalledTimes(1)
vi.advanceTimersToNextTimer()
vi.advanceTimersByTime(LINUX_LID_SLEEP_ASSERTION_RETRY_MS)
assertion.stop('status-change')
assertion.start('status-change')
failLatestChild()
expect(logger.warn).toHaveBeenCalledTimes(2)
assertion.dispose()
} finally {
vi.useRealTimers()
}
})
it('treats an inhibitor that held for a full retry interval as recovered', () => {
vi.useFakeTimers()
try {
const { assertion, failLatestChild, logger } = createRetryingAssertion()
assertion.start('status-change')
for (let attempt = 0; attempt < 4; attempt++) {
failLatestChild()
vi.advanceTimersToNextTimer()
}
vi.advanceTimersByTime(LINUX_LID_SLEEP_ASSERTION_RETRY_MS)
failLatestChild()
expect(logger.warn).toHaveBeenCalledTimes(2)
assertion.dispose()
} finally {
vi.useRealTimers()
}
})
})
})
+16 -5
View File
@@ -43,6 +43,7 @@ export class LinuxLidSleepAssertion {
private readonly platform: NodeJS.Platform
private readonly spawn: SystemdInhibitSpawn
private child: SystemdInhibitProcess | null = null
private childStartedAt: number | null = null
private retryNotBefore: number | null = null
private retryTimer: ReturnType<typeof setTimeout> | null = null
private systemdInhibitUnavailable = false
@@ -94,6 +95,7 @@ export class LinuxLidSleepAssertion {
}
this.child = child
this.childStartedAt = this.now()
const onError: SystemdInhibitErrorListener = (error) => {
this.handleChildFailure(
child,
@@ -119,18 +121,19 @@ export class LinuxLidSleepAssertion {
child.on('exit', onExit)
child.on('close', onClose)
child.stdin?.on('error', onError)
// Not a held lock yet: systemd-inhibit can still exit 1 right after spawning.
this.resetRetrySuppression()
this.resetFailureStreak()
}
stop(_reason: string): void {
this.resetRetrySuppression()
this.resetFailureStreak()
if (!this.child) {
return
}
const child = this.child
this.resetFailureStreakIfHeld()
this.child = null
this.childStartedAt = null
// Pipe and child errors can arrive after stop; close ends both event sources.
this.intentionalStops.add(child)
try {
@@ -166,7 +169,9 @@ export class LinuxLidSleepAssertion {
}
this.reportedFailures.add(child)
if (this.child === child) {
this.resetFailureStreakIfHeld()
this.child = null
this.childStartedAt = null
}
this.handleFailure(failureKey, startReason, details, failureType)
}
@@ -218,9 +223,15 @@ export class LinuxLidSleepAssertion {
this.logger.warn('[agent-awake] Linux lid sleep assertion failed', payload)
}
private resetFailureStreak(): void {
this.lastFailureKey = null
this.warnedForLastFailure = false
// Only a lock held for a retry interval ends the streak; idle/active cycles keep it.
private resetFailureStreakIfHeld(): void {
if (
this.childStartedAt !== null &&
this.now() - this.childStartedAt >= LINUX_LID_SLEEP_ASSERTION_RETRY_MS
) {
this.lastFailureKey = null
this.warnedForLastFailure = false
}
}
private scheduleRetry(): void {