ai-or-die 0.1.95 → 0.1.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/ai-or-die.js CHANGED
@@ -46,7 +46,8 @@ program
46
46
  .option('--sticky-notes-model <url>', 'override the sticky-note model GGUF download URL')
47
47
  .option('--sticky-notes-threads <number>', 'CPU threads for sticky-note inference (default: auto — three-quarters of the cores on CPU, gentle on GPU)')
48
48
  .option('--no-keepalive', 'disable keeping the machine awake while the server runs (Windows only; on by default)')
49
- .option('--keepalive-display', 'also keep the display on (default keeps the system awake but lets the monitor sleep)');
49
+ .option('--keepalive-display', 'also keep the display on (default keeps the system awake but lets the monitor sleep)')
50
+ .option('--disable-hibernation', 'disable OS hibernation + set sleep/hibernate timeouts to Never via elevated powercfg (Windows only; off by default; prompts for UAC; needed to stop host-initiated hibernation on Hyper-V guests)');
50
51
 
51
52
  // Auto-open is OFF by default and opt-in via --open. Legacy callers may still pass
52
53
  // --no-open (the old opt-out flag); filter it out so it parses harmlessly as a no-op.
@@ -138,6 +139,10 @@ async function main() {
138
139
  // the display on.
139
140
  keepalive: options.keepalive !== false && process.env.AIORDIE_DISABLE_KEEPALIVE !== '1',
140
141
  keepaliveDisplay: options.keepaliveDisplay === true || process.env.AIORDIE_KEEPALIVE_DISPLAY === '1',
142
+ // Opt-in: disable OS hibernation (+ sleep/hibernate timeouts -> Never) via
143
+ // an elevated powercfg. Off by default. The wake assertion can't block a
144
+ // host-initiated hibernate (Hyper-V vmicshutdown); this removes the target.
145
+ disableHibernation: options.disableHibernation === true || process.env.AIORDIE_DISABLE_HIBERNATION === '1',
141
146
  // Mesh binds loopback-only always: the tailnet `serve` proxy reaches the
142
147
  // port, the LAN never does (even with --https). Other modes: all interfaces.
143
148
  bindHost: options.mesh ? '127.0.0.1' : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-or-die",
3
- "version": "0.1.95",
3
+ "version": "0.1.96",
4
4
  "description": "Universal AI coding terminal — Claude, Copilot, Gemini & more in your browser",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+
3
+ const childProcess = require('child_process');
4
+ const KeepaliveManager = require('./keepalive-manager');
5
+
6
+ // Opt-in remediation for HOST-INITIATED sleep on Windows (primarily Hyper-V
7
+ // guests). The keepalive wake assertion (SetThreadExecutionState /
8
+ // PowerRequestSystemRequired) only suppresses the IDLE timer; Windows documents
9
+ // that it "cannot be used to prevent ... standby or hibernate" when the suspend
10
+ // is requested programmatically. On a Hyper-V guest, the host's Guest Shutdown
11
+ // Service (`vmicshutdown`) requests hibernation through the Application API,
12
+ // which the assertion cannot block. The only reliable fix is to remove the sleep
13
+ // target itself: `powercfg /hibernate off` (+ sleep/hibernate timeouts -> Never).
14
+ // On a Hyper-V guest S3 standby is typically unavailable, so disabling S4
15
+ // hibernate eliminates the vector entirely.
16
+ //
17
+ // That is a PRIVILEGED, PERSISTENT machine change (survives our process exit;
18
+ // reversible with `powercfg /hibernate on`), so it is OFF by default and gated
19
+ // behind --disable-hibernation / AIORDIE_DISABLE_HIBERNATION=1. When enabled, one
20
+ // long-lived in-box `powershell.exe` (spawned like the keepalive helper) runs
21
+ // ONCE at startup:
22
+ // 1. Reads HKLM\...\Power\HibernateEnabled (non-privileged). If already 0 it
23
+ // prints SKIPPED and exits -- no UAC prompt, so a machine that is already
24
+ // configured never prompts again.
25
+ // 2. Otherwise it launches an ELEVATED cmd.exe via `Start-Process -Verb RunAs`
26
+ // (UAC is the OS approve/deny; the flag is the user's pre-approval) that runs
27
+ // the powercfg remediation, then prints APPLIED / DENIED / ERROR.
28
+ // Best-effort and non-fatal: a denied UAC prompt, a headless/remote session with
29
+ // no interactive desktop, or any spawn failure logs a warning and the server
30
+ // continues normally. Never throws into startup. Windows-only; instant no-op on
31
+ // macOS/Linux. See docs/specs/keepalive.md and docs/adrs/0030-hibernation-guard.md.
32
+
33
+ // The remediation, chained under ONE elevation (one UAC prompt) via `cmd /c a & b`.
34
+ // All constants (no user input), so there is no command-injection surface. `& `
35
+ // runs every command regardless of a prior one's exit; success is judged by
36
+ // re-reading HibernateEnabled afterward (not the chain's exit code, which only
37
+ // reflects the last command), so an early failure can't be masked.
38
+ const REMEDIATION = [
39
+ 'powercfg /hibernate off',
40
+ 'powercfg /change standby-timeout-ac 0',
41
+ 'powercfg /change standby-timeout-dc 0',
42
+ 'powercfg /change hibernate-timeout-ac 0',
43
+ 'powercfg /change hibernate-timeout-dc 0',
44
+ ];
45
+
46
+ const READY_TIMEOUT_MS = 60000;
47
+
48
+ class HibernationGuard {
49
+ constructor(options = {}) {
50
+ this._enabled = !!options.enabled;
51
+ this._platform = options.platform || process.platform;
52
+ this._spawn = options.spawn || childProcess.spawn;
53
+ this._logger = options.logger || console;
54
+ this._readyTimeoutMs = options.readyTimeoutMs || READY_TIMEOUT_MS;
55
+
56
+ this._ran = false;
57
+ this._child = null;
58
+ }
59
+
60
+ // ---- pure builders (static so tests assert them without spawning) ----
61
+
62
+ static buildScript(ppid = process.pid) {
63
+ const argline = '/c ' + REMEDIATION.join(' & ');
64
+ return [
65
+ // 'Stop' turns a blocked/failed cmdlet into a caught terminating error
66
+ // rather than a silent partial run.
67
+ `$ErrorActionPreference = 'Stop'`,
68
+ // Trusted-integer parent-PID tag for triage (never user input).
69
+ `# aod-hibernation ppid=${ppid}`,
70
+ // (1) Non-privileged detect. Skip (no UAC) when hibernate is already off.
71
+ `$he = $null`,
72
+ `try { $he = (Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Power' -Name 'HibernateEnabled' -ErrorAction Stop).HibernateEnabled } catch { $he = $null }`,
73
+ `if ($he -eq 0) { [Console]::Out.WriteLine('SKIPPED'); exit 0 }`,
74
+ // (2) One elevated cmd.exe runs the whole powercfg chain under a single UAC
75
+ // prompt. Absolute cmd path (PATH-hijack hardening). WaitForExit + ExitCode
76
+ // is more reliable than -Wait with -Verb RunAs for reading the result.
77
+ `$cmd = Join-Path $env:SystemRoot 'System32\\cmd.exe'`,
78
+ `$argline = '${argline}'`,
79
+ `try {`,
80
+ ` $p = Start-Process -FilePath $cmd -Verb RunAs -WindowStyle Hidden -PassThru -ArgumentList $argline`,
81
+ ` $p.WaitForExit()`,
82
+ // Verify the critical end-state directly rather than trusting the exit code:
83
+ // `cmd /c a & b & c` reports only the LAST command's exit, so an early
84
+ // failure could be masked. APPLIED iff hibernation is now actually off.
85
+ ` $after = $null`,
86
+ ` try { $after = (Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Power' -Name 'HibernateEnabled' -ErrorAction Stop).HibernateEnabled } catch { $after = $null }`,
87
+ ` if ($after -eq 0) { [Console]::Out.WriteLine('APPLIED') } else { [Console]::Out.WriteLine('ERROR'); [Console]::Error.WriteLine('hibernate still enabled after powercfg (exit=' + $p.ExitCode + ')') }`,
88
+ `} catch {`,
89
+ // Detect a declined UAC by the Win32 error code (1223 = ERROR_CANCELLED),
90
+ // locale-independent; walk the inner-exception chain, then fall back to text.
91
+ ` $cancelled = $false`,
92
+ ` $e = $_.Exception`,
93
+ ` while ($null -ne $e) { if (($e -is [System.ComponentModel.Win32Exception]) -and ($e.NativeErrorCode -eq 1223)) { $cancelled = $true; break }; $e = $e.InnerException }`,
94
+ ` if (-not $cancelled -and $_.Exception.Message -match 'cancel') { $cancelled = $true }`,
95
+ ` if ($cancelled) { [Console]::Out.WriteLine('DENIED') } else { [Console]::Out.WriteLine('ERROR'); [Console]::Error.WriteLine($_.Exception.Message) }`,
96
+ `}`,
97
+ ].join('\n');
98
+ }
99
+
100
+ static buildArgs(ppid = process.pid) {
101
+ return [
102
+ '-NoProfile',
103
+ '-NonInteractive',
104
+ '-ExecutionPolicy', 'Bypass',
105
+ '-Command', HibernationGuard.buildScript(ppid),
106
+ ];
107
+ }
108
+
109
+ // Attempt the remediation once. No-op unless enabled on win32. Idempotent.
110
+ // Fire-and-forget: the server never awaits this (the UAC prompt is interactive
111
+ // and may take a while). Never throws.
112
+ run() {
113
+ if (this._ran) return;
114
+ if (!this._enabled || this._platform !== 'win32') return;
115
+ this._ran = true;
116
+
117
+ try {
118
+ const ps = KeepaliveManager.powershellPath();
119
+ const args = HibernationGuard.buildArgs();
120
+ const child = this._spawn(ps, args, {
121
+ stdio: ['ignore', 'pipe', 'pipe'],
122
+ windowsHide: true,
123
+ shell: false,
124
+ });
125
+ this._child = child;
126
+
127
+ // Never let the helper pin the parent's event loop.
128
+ this._safe(() => child.unref());
129
+ this._safe(() => child.stdout && child.stdout.unref && child.stdout.unref());
130
+ this._safe(() => child.stderr && child.stderr.unref && child.stderr.unref());
131
+
132
+ let out = '';
133
+ let err = '';
134
+ if (child.stdout) {
135
+ this._safe(() => child.stdout.setEncoding && child.stdout.setEncoding('utf8'));
136
+ child.stdout.on('data', (d) => { out += String(d); });
137
+ child.stdout.on('error', () => {});
138
+ }
139
+ if (child.stderr) {
140
+ this._safe(() => child.stderr.setEncoding && child.stderr.setEncoding('utf8'));
141
+ child.stderr.on('data', (d) => { err += String(d); });
142
+ child.stderr.on('error', () => {});
143
+ }
144
+
145
+ let settled = false;
146
+ const finish = (status, hint) => {
147
+ if (settled) return;
148
+ settled = true;
149
+ this._safe(() => clearTimeout(timer));
150
+ if (this._child === child) this._child = null;
151
+ this._logOutcome(status, hint);
152
+ };
153
+
154
+ // The UAC prompt blocks the helper until the user responds; only reap after
155
+ // a generous timeout so a genuinely stuck helper cannot linger forever.
156
+ const timer = setTimeout(() => {
157
+ finish('ERROR', 'timed out waiting for the elevation prompt');
158
+ this._safe(() => child.kill && child.kill());
159
+ }, this._readyTimeoutMs);
160
+ if (timer.unref) timer.unref();
161
+
162
+ child.on('error', (e) => finish('ERROR', (e && e.message) || 'spawn error'));
163
+ child.on('close', () => {
164
+ const m = out.match(/\b(APPLIED|SKIPPED|DENIED|ERROR)\b/);
165
+ finish(m ? m[1] : 'ERROR', err.trim());
166
+ });
167
+ } catch (err) {
168
+ this._child = null;
169
+ this._safe(() => this._logger.debug && this._logger.debug(
170
+ 'hibernation-guard: failed to start (continuing):', err && err.message));
171
+ }
172
+ }
173
+
174
+ _logOutcome(status, hint) {
175
+ const L = this._logger;
176
+ switch (status) {
177
+ case 'APPLIED':
178
+ this._safe(() => L.log && L.log(
179
+ 'hibernation guard: hibernation disabled + sleep/hibernate timeouts set to Never (host-initiated hibernation suppressed)'));
180
+ break;
181
+ case 'SKIPPED':
182
+ this._safe(() => L.log && L.log(
183
+ 'hibernation guard: hibernation already disabled — nothing to do'));
184
+ break;
185
+ case 'DENIED':
186
+ this._safe(() => L.warn && L.warn(
187
+ '⚠ hibernation guard: elevation declined — hibernation left enabled. A Hyper-V host can still hibernate this machine. Run `powercfg /hibernate off` as admin, or relaunch without --disable-hibernation.'));
188
+ break;
189
+ default:
190
+ this._safe(() => L.warn && L.warn(
191
+ `⚠ hibernation guard: could not disable hibernation${hint ? ` (${hint})` : ''}; the host may still hibernate this machine. Run \`powercfg /hibernate off\` as admin.`));
192
+ break;
193
+ }
194
+ }
195
+
196
+ _safe(fn) {
197
+ try { return fn(); } catch (_) { /* the guard must never throw into the caller */ }
198
+ }
199
+ }
200
+
201
+ module.exports = HibernationGuard;
202
+ module.exports.REMEDIATION = REMEDIATION;
@@ -6,15 +6,26 @@ const path = require('path');
6
6
  // Keep the host machine awake while the ai-or-die server runs (Windows 11).
7
7
  //
8
8
  // Mechanism: spawn ONE long-lived Windows PowerShell (5.1, in-box) helper that
9
- // P/Invokes SetThreadExecutionState from kernel32.dll to hold a power
10
- // assertion, then blocks on stdin. When the parent closes stdin (graceful
11
- // release) OR dies (the OS tears down the pipe -> ReadLine() returns null), the
12
- // helper clears the assertion and exits. Windows also drops a thread's
13
- // execution-state flags when the holding process dies, so even taskkill /F
14
- // cannot leak the assertion past reboot. No native deps, no powercfg.
9
+ // holds TWO redundant kernel32 power assertions, then blocks on stdin:
10
+ // 1. SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) -- the legacy
11
+ // per-thread assertion (shows in `powercfg /requests` with no reason text).
12
+ // 2. PowerCreateRequest + PowerSetRequest(PowerRequestSystemRequired) with a
13
+ // REASON_CONTEXT SimpleReasonString of "GitHub Copilot CLI session active".
14
+ // This is the exact API + reason string the GitHub Copilot CLI /keep-alive
15
+ // registers (its bundled cli-native.node calls the same kernel32 Power*
16
+ // functions with that wide string), so `powercfg /requests` shows a
17
+ // byte-identical "GitHub Copilot CLI session active" line.
18
+ // Holding both is deliberate belt-and-suspenders (chosen over Power*-only): if
19
+ // either API is refused at runtime the other still keeps the machine awake. The
20
+ // helper prints OK once AT LEAST ONE assertion is held. When the parent closes
21
+ // stdin (graceful release) OR dies (the OS tears down the pipe -> ReadLine()
22
+ // returns null), the helper clears both and exits. Windows also drops a thread's
23
+ // execution-state flags AND releases a process's power requests when the holding
24
+ // process dies, so even taskkill /F cannot leak past reboot. No native deps.
15
25
  //
16
26
  // Windows-only by design; an instant no-op on macOS/Linux. See
17
- // docs/specs/keepalive.md and docs/adrs/0028-windows-keepalive.md.
27
+ // docs/specs/keepalive.md and
28
+ // docs/adrs/0029-windows-keepalive-power-request.md (supersedes ADR-0028).
18
29
 
19
30
  // SetThreadExecutionState flags as DECIMAL uint32 literals. PowerShell 5.1
20
31
  // parses 0x80000001 as a negative Int32 and the [uint32] cast then throws, so
@@ -26,6 +37,15 @@ const ES_CONTINUOUS = 2147483648;
26
37
  const ES_SYSTEM = 2147483649;
27
38
  const ES_SYSTEM_DISPLAY = 2147483651;
28
39
 
40
+ // POWER_REQUEST_TYPE enum values passed to PowerSetRequest / PowerClearRequest.
41
+ // Small ints, so no PS 5.1 hex/Int32 parsing hazard (unlike the ES_ flags).
42
+ const PR_DISPLAY = 0; // PowerRequestDisplayRequired
43
+ const PR_SYSTEM = 1; // PowerRequestSystemRequired
44
+
45
+ // REASON_CONTEXT.SimpleReasonString shown by `powercfg /requests`. Byte-for-byte
46
+ // the string the GitHub Copilot CLI registers, so the entry is indistinguishable.
47
+ const KEEPALIVE_REASON = 'GitHub Copilot CLI session active';
48
+
29
49
  const READY_TIMEOUT_MS = 5000;
30
50
 
31
51
  class KeepaliveManager {
@@ -52,7 +72,25 @@ class KeepaliveManager {
52
72
  // ---- pure builders (static so tests assert them without spawning) ----
53
73
 
54
74
  static buildScript(displayRequired, ppid = process.pid) {
55
- const assert = displayRequired ? ES_SYSTEM_DISPLAY : ES_SYSTEM;
75
+ const stesAssert = displayRequired ? ES_SYSTEM_DISPLAY : ES_SYSTEM;
76
+ // One Add-Type carrying the REASON_CONTEXT struct (SIMPLE_STRING layout:
77
+ // Version + Flags + a single LPWStr) and the four kernel32 P/Invokes.
78
+ // Attributes are fully qualified so the compile never depends on Add-Type's
79
+ // default `using`s. The native REASON_CONTEXT is a union (SimpleReasonString
80
+ // XOR a larger Detailed struct); we only ever use SIMPLE_STRING, but two
81
+ // long pad fields make the marshalled struct >= the native union size
82
+ // (32 bytes x64 / 28 x86) so PowerCreateRequest can never read past the [In]
83
+ // buffer. Marshalling verified on a real Windows host.
84
+ const members = [
85
+ '[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]',
86
+ 'public struct REASON_CONTEXT { public uint Version; public uint Flags; [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)] public string Reason; public long Pad0; public long Pad1; }',
87
+ '[System.Runtime.InteropServices.DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint e);',
88
+ '[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] public static extern System.IntPtr PowerCreateRequest(ref REASON_CONTEXT Context);',
89
+ '[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] public static extern bool PowerSetRequest(System.IntPtr PowerRequest, int RequestType);',
90
+ '[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] public static extern bool PowerClearRequest(System.IntPtr PowerRequest, int RequestType);',
91
+ '[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(System.IntPtr hObject);',
92
+ ].join('\n');
93
+
56
94
  return [
57
95
  // 'Stop' is load-bearing: under Constrained Language Mode / WDAC /
58
96
  // AppLocker / EDR, Add-Type (which writes a .cs to %TEMP% and shells to
@@ -65,13 +103,39 @@ class KeepaliveManager {
65
103
  // input) so a stale/orphaned helper is identifiable for triage:
66
104
  // Get-CimInstance Win32_Process -Filter "Name='powershell.exe'"
67
105
  `# aod-keepalive ppid=${ppid}`,
68
- `Add-Type -Name P -Namespace W -MemberDefinition '[System.Runtime.InteropServices.DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint e);'`,
69
- // exit 1 when the assertion is refused so the helper never blocks on
70
- // stdin WITHOUT actually holding the assertion (which would otherwise
71
- // latch us "started" while the machine can still sleep).
72
- `if ([W.P]::SetThreadExecutionState([uint32]${assert}) -ne 0) { [Console]::Out.WriteLine('OK'); [Console]::Out.Flush() } else { [Console]::Error.WriteLine('SetThreadExecutionState returned 0'); exit 1 }`,
106
+ `Add-Type -Name P -Namespace W -MemberDefinition '${members}'`,
107
+ `$held = $false`,
108
+ // (1) Legacy per-thread assertion. Redundant safety net; carries no name in
109
+ // `powercfg /requests`.
110
+ `if ([W.P]::SetThreadExecutionState([uint32]${stesAssert}) -ne 0) { $held = $true }`,
111
+ // (2) Named power request -- this is the "GitHub Copilot CLI session active"
112
+ // line. Flags=1 is POWER_REQUEST_CONTEXT_SIMPLE_STRING; Version=0.
113
+ `$ctx = New-Object 'W.P+REASON_CONTEXT'`,
114
+ `$ctx.Version = 0`,
115
+ `$ctx.Flags = 1`,
116
+ `$ctx.Reason = '${KEEPALIVE_REASON}'`,
117
+ `$h = [W.P]::PowerCreateRequest([ref]$ctx)`,
118
+ // PowerCreateRequest returns NULL or INVALID_HANDLE_VALUE (-1) on failure.
119
+ `$hv = $h.ToInt64()`,
120
+ `$powerSys = $false`,
121
+ ...(displayRequired ? [`$powerDisp = $false`] : []),
122
+ // Set each request type independently so "at least one held" counts a
123
+ // display-only success too, and each is cleared only if it was set.
124
+ `if ($hv -ne 0 -and $hv -ne -1) {`,
125
+ ` if ([W.P]::PowerSetRequest($h, ${PR_SYSTEM})) { $held = $true; $powerSys = $true }`,
126
+ ...(displayRequired ? [` if ([W.P]::PowerSetRequest($h, ${PR_DISPLAY})) { $held = $true; $powerDisp = $true }`] : []),
127
+ `}`,
128
+ // Only latch "started" (block on stdin) when SOMETHING is actually held, so
129
+ // a total failure surfaces as ready=false instead of a silent awake-less
130
+ // block. Close a valid-but-unused handle before exiting so nothing lingers.
131
+ `if (-not $held) { if ($hv -ne 0 -and $hv -ne -1) { [void][W.P]::CloseHandle($h) } [Console]::Error.WriteLine('SetThreadExecutionState and PowerSetRequest both failed'); exit 1 }`,
132
+ `[Console]::Out.WriteLine('OK'); [Console]::Out.Flush()`,
73
133
  `while ($null -ne [Console]::In.ReadLine()) {}`,
134
+ // Release each held assertion on EOF; process death also drops all of them.
74
135
  `[void][W.P]::SetThreadExecutionState([uint32]${ES_CONTINUOUS})`,
136
+ `if ($powerSys) { [void][W.P]::PowerClearRequest($h, ${PR_SYSTEM}) }`,
137
+ ...(displayRequired ? [`if ($powerDisp) { [void][W.P]::PowerClearRequest($h, ${PR_DISPLAY}) }`] : []),
138
+ `if ($hv -ne 0 -and $hv -ne -1) { [void][W.P]::CloseHandle($h) }`,
75
139
  ].join('\n');
76
140
  }
77
141
 
@@ -206,8 +270,8 @@ class KeepaliveManager {
206
270
  const hint = firstErr || 'powershell.exe unavailable or blocked (Constrained Language Mode / WDAC / AV)';
207
271
  this._safe(() => this._logger.warn && this._logger.warn(
208
272
  `⚠ keepalive: could not hold the wake assertion; the machine may sleep (${hint}). Disable with --no-keepalive.`));
209
- // Reap a helper that timed out without exiting (e.g. SetThreadExecutionState
210
- // returned 0 and it is blocked on stdin) so it cannot leak.
273
+ // Reap a helper that timed out without exiting (e.g. both assertions were
274
+ // refused yet it is somehow blocked on stdin) so it cannot leak.
211
275
  if (this._child === child) this._safe(() => this.releaseSync());
212
276
  }
213
277
  }).catch(() => {});
package/src/server.js CHANGED
@@ -34,6 +34,7 @@ const CircularBuffer = require('./utils/circular-buffer');
34
34
  const MinHeap = require('./utils/eviction-heap');
35
35
  const RestartManager = require('./restart-manager');
36
36
  const KeepaliveManager = require('./keepalive-manager');
37
+ const HibernationGuard = require('./hibernation-guard');
37
38
  const { ControlEventBus, EVENT_KINDS: CONTROL_EVENT_KINDS } = require('./control/event-bus');
38
39
  const TranscriptBuffer = require('./sticky-note-transcript');
39
40
  const { createControlRouter } = require('./control/routes');
@@ -287,6 +288,13 @@ class ClaudeCodeWebServer {
287
288
  process.env.AIORDIE_DISABLE_KEEPALIVE !== '1',
288
289
  keepDisplayOn: !!options.keepaliveDisplay,
289
290
  });
291
+ // Opt-in, one-shot at startup: disable OS hibernation via elevated powercfg
292
+ // (Windows only). The wake assertion above only blocks the idle timer; a
293
+ // host-initiated hibernate (Hyper-V vmicshutdown) needs the target removed.
294
+ // Off by default; same underTest/CI gating as keepalive.
295
+ this.hibernationGuard = new HibernationGuard({
296
+ enabled: !!options.disableHibernation && !underTest && !isCI,
297
+ });
290
298
 
291
299
  this.sessionStore = new SessionStore(options.sessionStoreOptions);
292
300
  this.usageReader = new UsageReader(this.sessionDurationHours);
@@ -3552,6 +3560,9 @@ class ClaudeCodeWebServer {
3552
3560
  // Now listening — hold the OS awake for the server's lifetime
3553
3561
  // (Windows only; no-op elsewhere; never throws).
3554
3562
  this.keepaliveManager.start();
3563
+ // Opt-in one-shot: disable hibernation (elevated) so a Hyper-V host
3564
+ // can't hibernate us. Fire-and-forget; off by default; never throws.
3565
+ this.hibernationGuard.run();
3555
3566
  resolve(server);
3556
3567
  }
3557
3568
  };