feat: added macOS power assertion module to prevent idle-sleep during sessions

- Added MacOSPowerAssertion native module to prevent idle-sleep on macOS during active sessions.
- Integrated power assertion lifecycle management into coding-agent session with automatic start/stop.
- Implemented N-API bindings for macOS IOKit power assertions with RAII cleanup and cross-platform no-op fallback.
- Reformatted indentation from spaces to tabs across Rust chunk module for consistency.
This commit is contained in:
can1357
2026-04-07 03:45:18 +02:00
parent ad4c5917a4
commit 567197c34e
10 changed files with 380 additions and 109 deletions
+1
View File
@@ -22,6 +22,7 @@
### Changed
- Agent sessions now hold a macOS power assertion for their lifetime so the host stays awake during active coding-agent runs
- Updated chunk edit prompt documentation to use ellipsis (…) instead of ellipsis (...) for consistency in operation examples
- Modified chunk path parsing to preserve raw selector strings and extract CRC separately, enabling accurate chunk reference round-tripping in read/edit workflows
- Changed chunk edit behavior to auto-accept stale CRC checksums for subsequent operations on the same chunk within a batch, improving usability when applying multiple edits to the same target
@@ -49,7 +49,7 @@ import {
modelsAreEqual,
parseRateLimitReason,
} from "@oh-my-pi/pi-ai";
import type { SearchDb } from "@oh-my-pi/pi-natives";
import { MacOSPowerAssertion, type SearchDb } from "@oh-my-pi/pi-natives";
import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
import type { AsyncJob, AsyncJobManager } from "../async";
import type { Rule } from "../capability/rule";
@@ -396,6 +396,9 @@ export class AgentSession {
readonly sessionManager: SessionManager;
readonly settings: Settings;
readonly searchDb: SearchDb | undefined;
#powerAssertion: MacOSPowerAssertion | undefined;
readonly configWarnings: string[] = [];
#asyncJobManager: AsyncJobManager | undefined = undefined;
@@ -507,11 +510,36 @@ export class AgentSession {
#promptGeneration = 0;
#providerSessionState = new Map<string, ProviderSessionState>();
#startPowerAssertion(): void {
if (process.platform !== "darwin") {
return;
}
try {
this.#powerAssertion = MacOSPowerAssertion.start({ reason: "Oh My Pi agent session" });
} catch (error) {
logger.warn("Failed to acquire macOS power assertion", { error: String(error) });
}
}
#stopPowerAssertion(): void {
const assertion = this.#powerAssertion;
this.#powerAssertion = undefined;
if (!assertion) {
return;
}
try {
assertion.stop();
} catch (error) {
logger.warn("Failed to release macOS power assertion", { error: String(error) });
}
}
constructor(config: AgentSessionConfig) {
this.agent = config.agent;
this.sessionManager = config.sessionManager;
this.settings = config.settings;
this.searchDb = config.searchDb;
this.#startPowerAssertion();
this.#asyncJobManager = config.asyncJobManager;
this.#scopedModels = config.scopedModels ?? [];
this.#thinkingLevel = config.thinkingLevel;
@@ -1673,6 +1701,7 @@ export class AgentSession {
if (drained === false && deliveryState) {
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
}
this.#stopPowerAssertion();
await this.sessionManager.close();
this.#closeAllProviderSessions("dispose");
this.#unsubscribePendingActionPush?.();