Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
12 changes: 8 additions & 4 deletions src/daemon/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions src/daemon/settings/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
],
},
{
Expand Down
Loading