mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 08:07:18 +00:00
review(r1): scope display downgrade to macOS/Linux; document limits; polish
Adversarial review round 1 (agent-awake) findings: - MAJOR (cross-platform): Windows has no independent system-sleep floor. macOS (caffeinate -i -s) and Linux (systemd-inhibit) keep the system awake through the display downgrade, so the agent is never suspended there. Windows relies solely on the Electron blocker, and on Modern Standby laptops letting the display sleep can trigger connected-standby and suspend the agent. Fix: only downgrade to prevent-app-suspension where an independent system floor exists (darwin/linux); Windows keeps prevent-display-sleep. Revert the Windows copy (unchanged behavior) and add a Windows-no-downgrade test. - minor: document that a >15m hook-silent tool call lets the display sleep mid-work (known heuristic limitation). - minor: add a re-upgrade test (quiet system-only -> display-sleep on resume). - nits: thread the single captured now through getEligibleRunningStatusCount; fix zh 'agent' terminology (代理 -> 智能体) to match the rest of the settings. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -82,13 +82,17 @@ function createService(
|
||||
blocker = createBlocker(),
|
||||
macosAssertion = createMacosAssertion(),
|
||||
linuxAssertion = createLinuxAssertion(),
|
||||
powerMonitor: ReturnType<typeof createPowerMonitor> | null = null
|
||||
powerMonitor: ReturnType<typeof createPowerMonitor> | 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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -8588,7 +8588,7 @@
|
||||
},
|
||||
"agent-awake-copy": {
|
||||
"e5995ce268": "agents 作業中はコンピューターを起きたままにする",
|
||||
"95d3031db2": "エージェントの実行を継続できるよう、このコンピューターをスリープさせません。ディスプレイはエージェントがアクティブに作業している間は点灯し、長時間操作がない場合はスリープします。ふたを閉じたときの動作はデバイスの電源設定に従います。",
|
||||
"95d3031db2": "agents が作業している間、このコンピューターとディスプレイを起きたままにします。蓋を閉じたときの動作は、このデバイスの電源設定に従います。",
|
||||
"a42f6fbdd8": "エージェントの実行を継続できるよう、このコンピューターをスリープさせません。ディスプレイはエージェントがアクティブに作業している間は点灯し、長時間操作がない場合はスリープします。Orca はデバイスの電源ポリシーに従い、ふたを閉じた状態でもスリープしないよう要求します。"
|
||||
},
|
||||
"agent-status-hooks-copy": {
|
||||
|
||||
@@ -8551,7 +8551,7 @@
|
||||
},
|
||||
"agent-awake-copy": {
|
||||
"e5995ce268": "agents 작업 중 컴퓨터를 깨어 있게 유지",
|
||||
"95d3031db2": "에이전트가 계속 실행되도록 이 컴퓨터를 켜 둡니다. 디스플레이는 에이전트가 활발히 작업하는 동안 켜져 있으며, 오랫동안 활동이 없으면 절전될 수 있습니다. 덮개를 닫을 때의 동작은 기기의 전원 설정을 따릅니다.",
|
||||
"95d3031db2": "agents가 작업하는 동안 이 컴퓨터와 디스플레이를 깨어 있게 유지합니다. 뚜껑을 닫을 때의 동작은 이 기기의 전원 설정을 따릅니다.",
|
||||
"a42f6fbdd8": "에이전트가 계속 실행되도록 이 컴퓨터를 켜 둡니다. 디스플레이는 에이전트가 활발히 작업하는 동안 켜져 있으며, 오랫동안 활동이 없으면 절전될 수 있습니다. Orca는 기기의 전원 정책에 따라 덮개를 닫아도 절전되지 않도록 요청합니다."
|
||||
},
|
||||
"agent-status-hooks-copy": {
|
||||
|
||||
@@ -8551,8 +8551,8 @@
|
||||
},
|
||||
"agent-awake-copy": {
|
||||
"e5995ce268": "智能体工作时保持电脑唤醒",
|
||||
"95d3031db2": "保持此电脑唤醒,以便代理持续运行。显示器在代理活跃工作时保持点亮,在长时间无活动时可以进入睡眠。合盖行为取决于设备的电源设置。",
|
||||
"a42f6fbdd8": "保持此电脑唤醒,以便代理持续运行。显示器在代理活跃工作时保持点亮,在长时间无活动时可以进入睡眠。Orca 还会根据设备的电源策略,在合盖时请求设备保持唤醒。"
|
||||
"95d3031db2": "在智能体工作时保持此电脑和显示器唤醒。合盖行为遵循此设备的电源设置。",
|
||||
"a42f6fbdd8": "保持此电脑唤醒,以便智能体持续运行。显示器在智能体活跃工作时保持点亮,在长时间无活动时可以进入睡眠。Orca 还会根据设备的电源策略,在合盖时请求设备保持唤醒。"
|
||||
},
|
||||
"agent-status-hooks-copy": {
|
||||
"7707c15abb": "智能体状态钩子",
|
||||
|
||||
Reference in New Issue
Block a user