perf: hourly update check only with a browser open; opt-in retention, incremental auto-vacuum and Compact

- update-scheduler: default interval 1 h (DASHBOARD_UPDATE_CHECK_INTERVAL_MS and its 60 s floor kept);
  a scheduled tick with no /ws client is skipped, and the next browser to connect gets one catch-up
  check when the last one is stale (websocket.js onConnection). Manual check unchanged.
- db-retention.js: shared session purge (used by POST /api/settings/cleanup too), opt-in
  AGENTHUB_RETENTION_DAYS applied by the hourly maintenance in 25-session transactions with yields
  (max 5000/run), bounded incremental_vacuum when the freelist is large, and a manual Compact
  (VACUUM in a worker, converting to auto_vacuum=INCREMENTAL).
- db.js: new databases are INCREMENTAL; an existing one up to 32 MB is converted at startup.
- GET/PUT /api/settings/retention, POST /api/settings/compact (confirm: true); Settings → Data →
  Session cleanup gets the Automatic cleanup control and Compact now (en + vi).
This commit is contained in:
2026-10-16 10:20:38 +07:00
parent ecbbb4dd8f
commit e07993c536
21 changed files with 1323 additions and 58 deletions
+114
View File
@@ -11,6 +11,9 @@
* - GET /api/settings/backup (Settings)
* - PUT /api/settings/backup (Settings)
* - POST /api/settings/backup/run (Settings)
* - GET /api/settings/retention (Settings)
* - PUT /api/settings/retention (Settings)
* - POST /api/settings/compact (Settings)
* - GET /api/workflows/runs (Workflows)
* - GET /api/workflows/runs/{runId} (Workflows)
* - GET /api/remote-sources (Remote Sources)
@@ -1225,6 +1228,117 @@ const paths = {
},
},
"/api/settings/retention": (() => {
const state = {
description: "Automatic retention setting, database space and the last Compact",
content: {
"application/json": {
schema: {
type: "object",
required: ["days", "max_days", "env_owned", "compacting"],
properties: {
days: {
type: "integer",
minimum: 0,
maximum: 3650,
description:
"Finished sessions older than this many days are purged by the hourly maintenance; 0 = off (the default)",
},
max_days: { type: "integer" },
env_owned: { type: "object", properties: { days: { type: "boolean" } } },
last_run: {
type: "object",
nullable: true,
description:
"This process's last hourly maintenance: {at, retention?: {purged_sessions, purged_events, purged_agents, error?}, reclaim: {reclaimed_pages, error?}, ms}",
},
space: {
type: "object",
nullable: true,
properties: {
auto_vacuum: { type: "string", enum: ["none", "full", "incremental"] },
page_size: { type: "integer" },
page_count: { type: "integer" },
freelist_pages: { type: "integer" },
free_bytes: { type: "integer" },
},
},
db_size: { type: "integer", description: "Database file size in bytes" },
compacting: { type: "boolean" },
last_compact: {
type: "object",
nullable: true,
description:
"This process's last Compact: {at, ok, size_before?, size_after?, ms?, error?}",
},
},
},
},
},
};
return {
get: {
tags: ["Settings"],
summary: "Automatic retention and database space",
operationId: "getRetention",
responses: { 200: state },
},
put: {
tags: ["Settings"],
summary: "Set the automatic retention",
description:
"Persisted to the settings file as AGENTHUB_RETENTION_DAYS. 0 turns it off. Same-origin only; in server mode an admin's.",
operationId: "setRetention",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["days"],
properties: { days: { type: "integer", minimum: 0, maximum: 3650 } },
},
},
},
},
responses: {
200: { ...state, description: "The new state" },
400: { description: "INVALID_INPUT: days is not a whole number from 0 to 3650" },
403: { description: "EBADORIGIN: another site asked" },
409: { description: "EENVOWNED: the environment set AGENTHUB_RETENTION_DAYS" },
500: { description: "ESAVE: the settings file could not be written" },
},
},
};
})(),
"/api/settings/compact": {
post: {
tags: ["Settings"],
summary: "Compact the database now (full VACUUM)",
description:
"Rewrites the database in a worker thread, switching it to incremental auto_vacuum, then truncates the WAL. Hook writes wait while it runs. Answers at once; poll GET /api/settings/retention while `compacting`. Same-origin only; in server mode an admin's.",
operationId: "compactDatabase",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["confirm"],
properties: { confirm: { type: "boolean", enum: [true] } },
},
},
},
},
responses: {
202: { description: "Started (or already running); the retention state" },
400: { description: "ECONFIRM: confirm: true is missing" },
403: { description: "EBADORIGIN: another site asked" },
},
},
},
"/api/settings/codex-hooks": (() => {
const status = {
description: "Where AgentHub's Codex hooks stand",