daemon 空闲自动退出:30 分钟无调用释放内存,下次调用自动拉起

This commit is contained in:
luoxiaoxin123
2026-09-13 22:22:40 +08:00
parent b207dbcaa0
commit 4346f65d76
4 changed files with 18 additions and 2 deletions
+1 -1
View File
@@ -152,7 +152,7 @@ node ~/.zcode/gatekeeper/gatekeeper.mjs approve --last # 或直接批准最近
## 守护进程(daemon)
客户端每次调用先探测 `127.0.0.1:47811`(无副作用 ping),daemon 不在就自动拉起。收益:到审查端点的 TLS 连接常驻、决策缓存与任务状态驻内存、同请求并发去重、配置热重载(客户端每次请求带配置文件 mtime 签名,变了即重载)。
客户端每次调用先探测 `127.0.0.1:47811`(无副作用 ping),daemon 不在就自动拉起;**无调用 30 分钟后自动退出释放内存**(`daemonIdleMs` 可调,0 = 常驻),下次调用自动重新拉起。收益:到审查端点的 TLS 连接常驻、决策缓存与任务状态驻内存、同请求并发去重、配置热重载(客户端每次请求带配置文件 mtime 签名,变了即重载)。
```bash
node gatekeeper/gatekeeper.mjs shutdown-daemon # 停止(下次调用自动重新拉起)
+1
View File
@@ -16,6 +16,7 @@
"recentActions": 10,
"daemon": true,
"daemonPort": 47811,
"daemonIdleMs": 1800000,
"extraBody": {},
"toolAllowlist": [],
"autoMode": {
+4
View File
@@ -69,6 +69,7 @@ const DEFAULTS = {
recentActions: 10, // 提供给审查器的最近工具调用条数(组合风险判定)
daemon: true, // 守护进程模式(客户端自动拉起,连不上则内联降级)
daemonPort: 47811,
daemonIdleMs: 1800000, // daemon 空闲自动退出(ms):无调用 30 分钟后退出释放内存,下次调用自动拉起;0 = 常驻
extraBody: {}, // 深合并进 LLM 请求体的自定义字段(如 chat_template_kwargs.enable_thinking)
toolAllowlist: [],
autoMode: { allow: [], soft_deny: [], environment: [] }, // 用户自定义规则(非空则整段替换默认)
@@ -91,6 +92,9 @@ export function loadAll() {
if (process.env.GATEKEEPER_ON_UNCERTAIN) {
config.onUncertain = String(process.env.GATEKEEPER_ON_UNCERTAIN).toLowerCase();
}
if (process.env.GATEKEEPER_DAEMON_IDLE_MS != null) {
config.daemonIdleMs = parseInt(process.env.GATEKEEPER_DAEMON_IDLE_MS, 10) || 0;
}
// 超时预算钳制:LLM 最坏耗时(超时×(重试+1))+ 5s 余量不得超过 35s,
// 否则内联降级会撞上 hook 超时(PreToolUse 50s / PermissionRequest 45s)被掐成无决策。
const worst = (config.llmTimeoutMs | 0) * ((config.retries | 0) + 1);
+12 -1
View File
@@ -67,12 +67,22 @@ async function handleHook(req) {
// allowHalfOpen: true —— 客户端会半关闭(write+end),hook 判定是异步的(LLM 数秒),
// 默认模式下 Node 会在收到 FIN 时自动关闭本端,响应就写进了一条已死的连接。
// 空闲自动退出:ZCode 关闭/无调用一段时间后自动退出释放内存(~55MB),下次 hook 调用会自动重新拉起
let idleTimer = null;
function armIdle() {
const ms = all.config.daemonIdleMs | 0;
if (idleTimer) clearTimeout(idleTimer);
if (ms <= 0) return;
idleTimer = setTimeout(() => { log('idle ' + Math.round(ms / 1000) + 's — exit to free memory'); process.exit(0); }, ms);
}
const server = net.createServer({ allowHalfOpen: true }, (socket) => {
let buf = '';
socket.setEncoding('utf8');
socket.setTimeout(42_000, () => socket.destroy()); // 兜底:不允许超过 hook 超时
const dispatch = async (line) => {
armIdle(); // 任何请求(含 ping)都重置空闲计时
// 协议异常/未知 op 一律返回 {ok:false}(无 exitCode 字段)——客户端据此降级内联做完整审查,
// 绝不能返回 {exitCode:0}(会被当作"明确放行"决策,形成 fail-open)。
let res = { ok: false, error: 'unknown op' };
@@ -118,7 +128,8 @@ server.on('error', async (e) => {
});
server.listen(PORT, '127.0.0.1', () => {
log('daemon listening on 127.0.0.1:' + PORT + ' (model=' + all.config.model + ', guard=' + all.config.fullAccessGuard + ')');
log('daemon listening on 127.0.0.1:' + PORT + ' (model=' + all.config.model + ', guard=' + all.config.fullAccessGuard + ', idleExit=' + Math.round((all.config.daemonIdleMs|0)/1000) + 's)');
armIdle(); // 启动即计时:无任何调用则按时退出
// 预热:开一条到审查端点的 TLS 连接,让第一次真实审查免握手
setTimeout(async () => {
try {