diff --git a/src/main/agent-awake-service.test.ts b/src/main/agent-awake-service.test.ts index a56738f6c60..51c67f489ff 100644 --- a/src/main/agent-awake-service.test.ts +++ b/src/main/agent-awake-service.test.ts @@ -82,13 +82,17 @@ function createService( blocker = createBlocker(), macosAssertion = createMacosAssertion(), linuxAssertion = createLinuxAssertion(), - powerMonitor: ReturnType | null = null + powerMonitor: ReturnType | null = null, + // Default to darwin (has an independent caffeinate system floor) so the + // display-downgrade path is exercised deterministically regardless of CI OS. + platform: NodeJS.Platform = 'darwin' ): AgentAwakeService { return new AgentAwakeService({ blocker, linuxAssertion, macosAssertion, now, + platform, powerMonitor, logger: { debug: vi.fn(), @@ -173,6 +177,45 @@ describe('AgentAwakeService', () => { service.dispose() }) + it('re-upgrades to prevent-display-sleep when a quiet agent resumes producing status', () => { + const blocker = createBlocker() + let now = 1_000 + AGENT_AWAKE_DISPLAY_KEEP_AFTER_MS + 1 + const service = createService(() => now, blocker) + + service.setEnabled(true) + // Starts in the quiet tier: system-only. + service.setStatuses([workingStatus({ receivedAt: 1_000 })]) + expect(blocker.start).toHaveBeenLastCalledWith('prevent-app-suspension') + + // A fresh status arrives (agent resumed) -> back to display-sleep. + service.setStatuses([workingStatus({ receivedAt: now })]) + expect(blocker.start).toHaveBeenLastCalledWith('prevent-display-sleep') + service.dispose() + }) + + it('never downgrades the display on Windows (no independent system-sleep floor)', () => { + const blocker = createBlocker() + // Quiet past the display window, but on Windows the display assertion is the + // only thing keeping the system awake, so it must NOT be downgraded. + const now = 1_000 + AGENT_AWAKE_DISPLAY_KEEP_AFTER_MS + 1 + const service = createService( + () => now, + blocker, + createMacosAssertion(), + createLinuxAssertion(), + null, + 'win32' + ) + + service.setEnabled(true) + service.setStatuses([workingStatus({ receivedAt: 1_000 })]) + + expect(blocker.start).toHaveBeenCalledTimes(1) + expect(blocker.start).toHaveBeenCalledWith('prevent-display-sleep') + expect(blocker.start).not.toHaveBeenCalledWith('prevent-app-suspension') + service.dispose() + }) + it('starts and stops from settings flips around an already-running status', () => { const blocker = createBlocker() const macosAssertion = createMacosAssertion() diff --git a/src/main/agent-awake-service.ts b/src/main/agent-awake-service.ts index 3fad2c0b629..63c1f055f50 100644 --- a/src/main/agent-awake-service.ts +++ b/src/main/agent-awake-service.ts @@ -13,6 +13,14 @@ export const AGENT_AWAKE_STATUS_STALE_AFTER_MS = 2 * 60 * 60 * 1000 // refreshed within this shorter window, downgrade to a system-only assertion: // the machine (and the agent) stays awake for the full 2h window, but the // display is allowed to sleep normally. +// +// Known limitation: "no status for 15m" is a proxy for "idle", so a single +// hook-silent tool call longer than this (e.g. a 30m build streaming output) +// will let the display sleep mid-work; press a key to wake it (the agent keeps +// running). Refreshing this off raw PTY output would keep such screens lit but +// costs the battery win (a spinner counts as output), so it's left as future +// work. The downgrade only applies where a separate system-sleep floor exists +// (macOS/Linux); Windows keeps prevent-display-sleep (see refresh()). export const AGENT_AWAKE_DISPLAY_KEEP_AFTER_MS = 15 * 60 * 1000 type AgentAwakeBlockerType = 'prevent-app-suspension' | 'prevent-display-sleep' @@ -48,6 +56,7 @@ type AgentAwakeServiceOptions = { logger?: Logger macosAssertion?: PlatformAwakeAssertion now?: () => number + platform?: NodeJS.Platform powerMonitor?: PowerMonitorEventSource | null } @@ -62,12 +71,14 @@ export class AgentAwakeService { private readonly logger: Logger private readonly macosAssertion: PlatformAwakeAssertion private readonly now: () => number + private readonly platform: NodeJS.Platform private readonly unsubscribeResume: (() => void) | null constructor(options: AgentAwakeServiceOptions = {}) { this.blocker = options.blocker ?? powerSaveBlocker this.logger = options.logger ?? console this.now = options.now ?? Date.now + this.platform = options.platform ?? process.platform // Windows lid close is intentionally not modeled as an assertion here: // keeping it awake requires mutating the user's global power plan. this.linuxAssertion = @@ -118,15 +129,22 @@ export class AgentAwakeService { private refresh(reason: string): void { this.scheduleStaleTimer() const now = this.now() - const runningStatusCount = this.getEligibleRunningStatusCount() + const runningStatusCount = this.getEligibleRunningStatusCount(now) const shouldBlock = this.enabled && runningStatusCount > 0 if (shouldBlock) { // Keep the screen lit only while an agent is actively working; once every // working status has gone quiet past the display window, keep the system - // (and the agent) awake but let the display sleep. + // (and the agent) awake but let the display sleep. Only downgrade where a + // SEPARATE system-sleep floor is held (macOS caffeinate / Linux + // systemd-inhibit); on Windows there is no such floor, and letting the + // display sleep can trigger Modern Standby and suspend the agent, so keep + // prevent-display-sleep there — the display assertion is load-bearing. + const hasIndependentSystemFloor = this.platform === 'darwin' || this.platform === 'linux' const displayEligibleCount = this.getDisplayEligibleCount(now) const desiredType: AgentAwakeBlockerType = - displayEligibleCount > 0 ? 'prevent-display-sleep' : 'prevent-app-suspension' + displayEligibleCount > 0 || !hasIndependentSystemFloor + ? 'prevent-display-sleep' + : 'prevent-app-suspension' this.startBlocker(reason, runningStatusCount, desiredType) this.startMacosAssertion(reason) this.startLinuxAssertion(reason) @@ -137,8 +155,7 @@ export class AgentAwakeService { } } - private getEligibleRunningStatusCount(): number { - const now = this.now() + private getEligibleRunningStatusCount(now: number): number { return this.statuses.filter((status) => this.isWakeEligible(status, now)).length } diff --git a/src/renderer/src/components/settings/AgentsPane.test.tsx b/src/renderer/src/components/settings/AgentsPane.test.tsx index b349474f5fa..992cbaf2b34 100644 --- a/src/renderer/src/components/settings/AgentsPane.test.tsx +++ b/src/renderer/src/components/settings/AgentsPane.test.tsx @@ -217,7 +217,7 @@ describe('AgentsPane', () => { it('describes Windows lid behavior according to the device', () => { expect(getAgentAwakeDescription('Windows')).toBe( - "Keeps this computer awake so agents keep running. The display stays on while agents are actively working and is allowed to sleep during long quiet stretches. Lid-close behavior follows this device's power settings." + "Keeps this computer and display awake while agents are working. Lid-close behavior follows this device's power settings." ) }) diff --git a/src/renderer/src/components/settings/agent-awake-copy.ts b/src/renderer/src/components/settings/agent-awake-copy.ts index 94b0d67bc3b..b7274044adc 100644 --- a/src/renderer/src/components/settings/agent-awake-copy.ts +++ b/src/renderer/src/components/settings/agent-awake-copy.ts @@ -13,9 +13,11 @@ export function getAgentAwakeDescription( userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent ): string { if (userAgent.includes('Windows')) { + // Windows keeps prevent-display-sleep (no display downgrade — see + // agent-awake-service.ts), so the display stays awake as before. return translate( AGENT_AWAKE_DESCRIPTION_WINDOWS_KEY, - "Keeps this computer awake so agents keep running. The display stays on while agents are actively working and is allowed to sleep during long quiet stretches. Lid-close behavior follows this device's power settings." + "Keeps this computer and display awake while agents are working. Lid-close behavior follows this device's power settings." ) } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 3da1b252461..e8c69c8c61e 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -8611,7 +8611,7 @@ }, "agent-awake-copy": { "e5995ce268": "Keep computer awake while agents are working", - "95d3031db2": "Keeps this computer awake so agents keep running. The display stays on while agents are actively working and is allowed to sleep during long quiet stretches. Lid-close behavior follows this device's power settings.", + "95d3031db2": "Keeps this computer and display awake while agents are working. Lid-close behavior follows this device's power settings.", "a42f6fbdd8": "Keeps this computer awake so agents keep running. The display stays on while agents are actively working and is allowed to sleep during long quiet stretches. Orca also asks this device to stay awake when the lid is closed, subject to its power policy." }, "agent-status-hooks-copy": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 6aeca5806af..de9f086ab30 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -8566,7 +8566,7 @@ }, "agent-awake-copy": { "e5995ce268": "Mantener la computadora activa mientras los agentes están trabajando", - "95d3031db2": "Mantiene este equipo activo para que los agentes sigan ejecutándose. La pantalla permanece encendida mientras los agentes trabajan activamente y puede apagarse durante periodos largos de inactividad. El comportamiento al cerrar la tapa depende de la configuración de energía del dispositivo.", + "95d3031db2": "Mantiene esta computadora y la pantalla activas mientras los agentes están trabajando. El comportamiento al cerrar la tapa sigue la configuración de energía de este dispositivo.", "a42f6fbdd8": "Mantiene este equipo activo para que los agentes sigan ejecutándose. La pantalla permanece encendida mientras los agentes trabajan activamente y puede apagarse durante periodos largos de inactividad. Orca también solicita que el dispositivo permanezca activo con la tapa cerrada, según su política de energía." }, "agent-status-hooks-copy": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index ea624f3390c..64f75c5408e 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -8588,7 +8588,7 @@ }, "agent-awake-copy": { "e5995ce268": "agents 作業中はコンピューターを起きたままにする", - "95d3031db2": "エージェントの実行を継続できるよう、このコンピューターをスリープさせません。ディスプレイはエージェントがアクティブに作業している間は点灯し、長時間操作がない場合はスリープします。ふたを閉じたときの動作はデバイスの電源設定に従います。", + "95d3031db2": "agents が作業している間、このコンピューターとディスプレイを起きたままにします。蓋を閉じたときの動作は、このデバイスの電源設定に従います。", "a42f6fbdd8": "エージェントの実行を継続できるよう、このコンピューターをスリープさせません。ディスプレイはエージェントがアクティブに作業している間は点灯し、長時間操作がない場合はスリープします。Orca はデバイスの電源ポリシーに従い、ふたを閉じた状態でもスリープしないよう要求します。" }, "agent-status-hooks-copy": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index b2496dd29c2..7820bac2c67 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -8551,7 +8551,7 @@ }, "agent-awake-copy": { "e5995ce268": "agents 작업 중 컴퓨터를 깨어 있게 유지", - "95d3031db2": "에이전트가 계속 실행되도록 이 컴퓨터를 켜 둡니다. 디스플레이는 에이전트가 활발히 작업하는 동안 켜져 있으며, 오랫동안 활동이 없으면 절전될 수 있습니다. 덮개를 닫을 때의 동작은 기기의 전원 설정을 따릅니다.", + "95d3031db2": "agents가 작업하는 동안 이 컴퓨터와 디스플레이를 깨어 있게 유지합니다. 뚜껑을 닫을 때의 동작은 이 기기의 전원 설정을 따릅니다.", "a42f6fbdd8": "에이전트가 계속 실행되도록 이 컴퓨터를 켜 둡니다. 디스플레이는 에이전트가 활발히 작업하는 동안 켜져 있으며, 오랫동안 활동이 없으면 절전될 수 있습니다. Orca는 기기의 전원 정책에 따라 덮개를 닫아도 절전되지 않도록 요청합니다." }, "agent-status-hooks-copy": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 7276811fb93..0969fba37b0 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -8551,8 +8551,8 @@ }, "agent-awake-copy": { "e5995ce268": "智能体工作时保持电脑唤醒", - "95d3031db2": "保持此电脑唤醒,以便代理持续运行。显示器在代理活跃工作时保持点亮,在长时间无活动时可以进入睡眠。合盖行为取决于设备的电源设置。", - "a42f6fbdd8": "保持此电脑唤醒,以便代理持续运行。显示器在代理活跃工作时保持点亮,在长时间无活动时可以进入睡眠。Orca 还会根据设备的电源策略,在合盖时请求设备保持唤醒。" + "95d3031db2": "在智能体工作时保持此电脑和显示器唤醒。合盖行为遵循此设备的电源设置。", + "a42f6fbdd8": "保持此电脑唤醒,以便智能体持续运行。显示器在智能体活跃工作时保持点亮,在长时间无活动时可以进入睡眠。Orca 还会根据设备的电源策略,在合盖时请求设备保持唤醒。" }, "agent-status-hooks-copy": { "7707c15abb": "智能体状态钩子",