feat: 지서버가 활성 모델일 때 절전 안 하도록 keep-alive 신호 추가
08-09에 이미 조사했던 "지서버가 30분 설정보다 훨씬 빨리 잔다" 문제는 사용자가 전기료 절약 관점에서 선호하는 동작으로 확정하고 조사를 접었던 건이다([[project_wol_gate]]). 그런데 오늘(08-10) 외부에서 실제로 사용 중일 때 이 빠른 절전이 매번 ~23초 기상 지연으로 이어져 불편해졌고, 사용자가 "지서버가 안자게 신호를 계속 보내자"고 명시적으로 요청했다 — 원인 재조사가 아니라 능동적 keep-alive를 원하는 것으로 확인. 원인 후보였던 "Windows 유휴 타이머가 마지막 사용자 입력 기준이라 API 트래픽으로는 리셋 안 됨"이 맞다면, 주기적으로 실제 HTTP 요청을 보내는 것 자체가 대응책이 된다. `llm.provider === 'ollama_local'`이고(getProvider()의 단일 캐시 구조상 어차피 이 조건에서만 실제 채팅 트래픽도 지서버로 간다) WS 클라이언트가 최소 1개 연결돼 있을 때만(앱이 실제로 열려 있을 때만) 2분마다 직결 엔드포인트에 가벼운 /api/tags 요청을 보낸다. 게이트가 아니라 직결로만 보내므로, 이미 잠들어 있으면 그냥 조용히 실패한다 — 깨우는 신호가 아니라 "깨어있는 동안 재우지 않는" 신호라, 나머지 시간대의 절전 이점(사용자가 이미 선택한)을 훼손하지 않는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* ollama-local-keepalive.ts
|
||||
*
|
||||
* Periodically pokes 지서버's Ollama endpoint while it's the active model AND someone
|
||||
* has the app open, so it doesn't drop to sleep mid-session.
|
||||
*
|
||||
* Why this exists (2026-08-10): 지서버 (Windows) sleeps much faster than its own 30-minute
|
||||
* idle setting because Windows' idle timer counts user input (keyboard/mouse), not inbound
|
||||
* network requests — a headless box that's only ever touched via WOL+Ollama API traffic
|
||||
* looks "idle" to Windows no matter how many chat turns are running. This was investigated
|
||||
* on 08-09 and the user decided the fast-sleep behavior was fine (power savings, wol-gate
|
||||
* absorbs the ~23s wake delay) — see [[project_wol_gate]]. It stopped being fine once the
|
||||
* user was relying on it while away from home (08-10): the wake delay repeating mid-session
|
||||
* was disruptive enough that they asked for an explicit keep-alive signal ("지서버가 안자게
|
||||
* 신호를 계속 보내자") rather than reopening the whole "why does it sleep early" investigation.
|
||||
*
|
||||
* This is a signal to keep it FROM sleeping, not a wake trigger — it hits the endpoint
|
||||
* directly (never through wol-gate), so if 지서버 is already asleep this just fails silently
|
||||
* and does nothing. Forcing a wake here would undo the power-saving behavior the user chose
|
||||
* to keep for the other 95% of the time this box isn't actively in use.
|
||||
*
|
||||
* Gated on two conditions so it can't run unattended and quietly cancel out the power
|
||||
* savings the user explicitly kept:
|
||||
* 1. llm.provider === 'ollama_local' — someone has actually selected 지서버 as the active
|
||||
* model right now (getProvider()'s single-instance cache means this is also the only
|
||||
* condition under which any real chat traffic would even reach it).
|
||||
* 2. At least one WebSocket client is connected — the app is actually open, not just left
|
||||
* selected on ollama_local from a previous session while the gateway keeps running.
|
||||
*/
|
||||
|
||||
import { getConfig } from '../config/config';
|
||||
|
||||
const KEEPALIVE_INTERVAL_MS = 2 * 60 * 1000;
|
||||
const PING_TIMEOUT_MS = 4_000;
|
||||
|
||||
let lastWasAwake: boolean | null = null;
|
||||
|
||||
async function pingIfActive(hasConnectedClient: () => boolean): Promise<void> {
|
||||
if (!hasConnectedClient()) return;
|
||||
let endpoint = '';
|
||||
try {
|
||||
const raw = getConfig().getConfig() as any;
|
||||
if (raw?.llm?.provider !== 'ollama_local') return;
|
||||
endpoint = String(raw?.llm?.providers?.ollama_local?.endpoint || '').trim();
|
||||
} catch { return; }
|
||||
if (!endpoint) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${endpoint}/api/tags`, { signal: AbortSignal.timeout(PING_TIMEOUT_MS) });
|
||||
const text = await res.text();
|
||||
const awake = !text.trimStart().startsWith('<'); // wol-gate's "waking up" page is HTML
|
||||
if (awake !== lastWasAwake) {
|
||||
console.log(`[ollama-local-keepalive] ${awake ? '지서버 응답 확인 (깨어있음)' : '지서버 무응답 — 절전 상태로 추정'}`);
|
||||
}
|
||||
lastWasAwake = awake;
|
||||
} catch {
|
||||
if (lastWasAwake !== false) console.log('[ollama-local-keepalive] ping failed — 지서버 무응답');
|
||||
lastWasAwake = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function startOllamaLocalKeepAlive(hasConnectedClient: () => boolean): NodeJS.Timeout {
|
||||
return setInterval(() => { pingIfActive(hasConnectedClient); }, KEEPALIVE_INTERVAL_MS);
|
||||
}
|
||||
@@ -107,6 +107,7 @@ import {
|
||||
import { getPaths } from '../config/paths';
|
||||
import { getVault, SecretValue } from '../security/vault';
|
||||
import { getOllamaClient } from '../agents/ollama-client';
|
||||
import { startOllamaLocalKeepAlive } from './ollama-local-keepalive';
|
||||
import { spawnAgent } from '../agents/spawner';
|
||||
import { getSession, addMessage, getHistory, getHistoryForApiCall, getWorkspace, setWorkspace, clearHistory, setContextTokens, cleanupSessions, cleanupEmptySessions, migrateGlobalSessionsToUser, listAllSessions, resolveNumCtx, activeOllamaEndpoint } from './session';
|
||||
import { hookBus } from './hooks/hooks';
|
||||
@@ -4707,6 +4708,8 @@ const wsPingInterval = setInterval(() => {
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
const ollamaLocalKeepAliveInterval = startOllamaLocalKeepAlive(() => !!wss && wss.clients.size > 0);
|
||||
|
||||
server.on('error', (err: any) => {
|
||||
if (err?.code === 'EADDRINUSE') {
|
||||
console.error(`[Gateway] Port ${HOST}:${PORT} is already in use.`);
|
||||
|
||||
Reference in New Issue
Block a user