diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 0fe43582..3025e1cc 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -81,6 +81,10 @@ CODEOID_MAX_SESSIONS_PER_HOUR=0 # session creations per subject per hour CODEOID_DAEMON_URL=ws://127.0.0.1:7400 # (for CLI + TUI client) CODEOID_DB_PATH=~/.codeoid/codeoid.db # SQLite path CODEOID_TRANSCRIPT_DIR=~/.codeoid/transcripts +CODEOID_RESUME_MAX_SESSIONS=200 # sessions restored from disk at startup, newest-first. + # Resume is also time-boxed, so raising this costs + # startup time; the remainder stays on disk and loads + # on a later restart. # Memory CODEOID_MEMORY=1 # default: on; set to 0 to disable diff --git a/src/config.ts b/src/config.ts index 1e27ab66..edc34a71 100644 --- a/src/config.ts +++ b/src/config.ts @@ -351,11 +351,21 @@ const SessionSchema = z * on big sessions. Legacy clients always get the full buffer. */ attachTailBytes: z.number().int().min(1024).default(512 * 1024), + /** + * How many sessions the daemon restores from disk on start, newest-first + * by last activity. Resume is ALSO time-boxed (RESUME_DEADLINE_MS), so + * this is the coarse guard and the deadline is the fine one: raising it + * on a box with many long-lived sessions costs startup time, not + * correctness. Sessions past the cap stay on disk and load on a later + * restart. + */ + resumeMaxSessions: z.number().int().min(1).default(200), }) .default({ turnStallTimeoutMs: 300_000, mcpToolTimeoutMs: 120_000, attachTailBytes: 512 * 1024, + resumeMaxSessions: 200, }) // Enforce the "SDK signals first" contract across BOTH fields — not just the // defaults. An env override / config file could otherwise set the MCP timeout @@ -1011,6 +1021,8 @@ export interface CodeoidConfig { mcpToolTimeoutMs?: number; /** Tail window (bytes) replayed on attach for `scrollback.paging` clients; older history is paged on demand. Defaults to 524288 (512 KiB) when omitted. */ attachTailBytes?: number; + /** Sessions restored from disk at daemon start, newest-first by last activity. Resume is also time-boxed, so this is the coarse guard; the remainder stays on disk. Defaults to 200 when omitted. */ + resumeMaxSessions?: number; }; /** * The per-tenant conductor session (fleet supervisor). Optional in the @@ -1235,6 +1247,7 @@ const ENV_OVERRIDES: readonly EnvOverride[] = [ { env: "CODEOID_PUSH_RELAY_TOKEN", path: "push.relayToken", kind: "string" }, { env: "CODEOID_TURN_STALL_TIMEOUT_MS", path: "session.turnStallTimeoutMs", kind: "int" }, { env: "CODEOID_MCP_TOOL_TIMEOUT_MS", path: "session.mcpToolTimeoutMs", kind: "int" }, + { env: "CODEOID_RESUME_MAX_SESSIONS", path: "session.resumeMaxSessions", kind: "int" }, // Embed-SSO trusted framing origins (comma-separated). Each is an exact // origin (scheme://host[:port]) permitted to frame the web UI and hand it a // credential via the URL hash. Empty ⇒ hash handoff disabled (safe default). diff --git a/src/daemon/session-manager.ts b/src/daemon/session-manager.ts index 8ed85d83..585602f2 100644 --- a/src/daemon/session-manager.ts +++ b/src/daemon/session-manager.ts @@ -288,7 +288,9 @@ function toFleetEventWire(row: DispatchEventRow): FleetEventWire { }; } -const RESUME_MAX_SESSIONS = 50; +/** Fallback when no config is supplied (tests, embedded use). The + * configured value is `session.resumeMaxSessions`. */ +const RESUME_MAX_SESSIONS_DEFAULT = 200; const RESUME_DEADLINE_MS = 20_000; /** Per-session transcript read budget on resume. Scrollback keeps at most * 20 MiB / 5000 messages — parsing history past that would be evicted on @@ -614,11 +616,13 @@ export class SessionManager { const allMetas = await this.#transcriptStore.loadAllMeta(); // Newest-first by last activity so the cap keeps the most relevant - // sessions when there are more than RESUME_MAX_SESSIONS on disk. + // sessions when there are more than the configured cap on disk. const sorted = [...allMetas].sort( (a, b) => resumeSortKey(b) - resumeSortKey(a), ); - const capped = sorted.slice(0, RESUME_MAX_SESSIONS); + const resumeMaxSessions = + this.#config?.session?.resumeMaxSessions ?? RESUME_MAX_SESSIONS_DEFAULT; + const capped = sorted.slice(0, resumeMaxSessions); // Goal config by orchestrator session id, built from EVERY meta on disk // rather than from `capped`. A child inside this boot's resume window whose // orchestrator fell outside it still needs its restrictions and its brief, @@ -782,7 +786,7 @@ mcpHub: this.#mcpHub, const droppedCap = sorted.length - capped.length; if (droppedCap > 0 || skippedDeadline > 0) { console.warn( - `[codeoid] resume: restored ${resumed} of ${sorted.length} session(s); ${droppedCap} left over the ${RESUME_MAX_SESSIONS}-session cap, ${skippedDeadline} skipped past the ${RESUME_DEADLINE_MS}ms deadline (still on disk; loadable on a future restart).`, + `[codeoid] resume: restored ${resumed} of ${sorted.length} session(s); ${droppedCap} left over the ${resumeMaxSessions}-session cap, ${skippedDeadline} skipped past the ${RESUME_DEADLINE_MS}ms deadline (still on disk; loadable on a future restart).`, ); } if (resumedChildren > 0) { diff --git a/src/daemon/settings/manifest.ts b/src/daemon/settings/manifest.ts index ca503d42..cb895b3a 100644 --- a/src/daemon/settings/manifest.ts +++ b/src/daemon/settings/manifest.ts @@ -76,6 +76,13 @@ const general: SettingsTab = { default: 524288, advanced: true, }), + cfg("session.resumeMaxSessions", "Resume session cap", "Sessions restored from disk on daemon start, newest-first. Resume is also time-boxed, so raising this costs startup time; sessions past the cap stay on disk.", { + kind: "int", + envVar: "CODEOID_RESUME_MAX_SESSIONS", + min: 1, + default: 200, + advanced: true, + }), ], }, {