2026-02-08 16:20:52 -08:00
|
|
|
|
import type { ChatType } from "../channels/chat-type.js";
|
2026-01-17 06:48:34 +00:00
|
|
|
|
|
2026-01-14 01:08:15 +00:00
|
|
|
|
export type ReplyMode = "text" | "command";
|
|
|
|
|
|
export type TypingMode = "never" | "instant" | "thinking" | "message";
|
|
|
|
|
|
export type SessionScope = "per-sender" | "global";
|
2026-01-27 21:51:23 -05:00
|
|
|
|
export type DmScope = "main" | "per-peer" | "per-channel-peer" | "per-account-channel-peer";
|
2026-01-14 01:08:15 +00:00
|
|
|
|
export type ReplyToMode = "off" | "first" | "all";
|
|
|
|
|
|
export type GroupPolicy = "open" | "disabled" | "allowlist";
|
|
|
|
|
|
export type DmPolicy = "pairing" | "allowlist" | "open" | "disabled";
|
|
|
|
|
|
|
|
|
|
|
|
export type OutboundRetryConfig = {
|
|
|
|
|
|
/** Max retry attempts for outbound requests (default: 3). */
|
|
|
|
|
|
attempts?: number;
|
|
|
|
|
|
/** Minimum retry delay in ms (default: 300-500ms depending on provider). */
|
|
|
|
|
|
minDelayMs?: number;
|
|
|
|
|
|
/** Maximum retry delay cap in ms (default: 30000). */
|
|
|
|
|
|
maxDelayMs?: number;
|
|
|
|
|
|
/** Jitter factor (0-1) applied to delays (default: 0.1). */
|
|
|
|
|
|
jitter?: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export type BlockStreamingCoalesceConfig = {
|
|
|
|
|
|
minChars?: number;
|
|
|
|
|
|
maxChars?: number;
|
|
|
|
|
|
idleMs?: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export type BlockStreamingChunkConfig = {
|
|
|
|
|
|
minChars?: number;
|
|
|
|
|
|
maxChars?: number;
|
|
|
|
|
|
breakPreference?: "paragraph" | "newline" | "sentence";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-23 17:56:50 +00:00
|
|
|
|
export type MarkdownTableMode = "off" | "bullets" | "code";
|
|
|
|
|
|
|
|
|
|
|
|
export type MarkdownConfig = {
|
|
|
|
|
|
/** Table rendering mode (off|bullets|code). */
|
|
|
|
|
|
tables?: MarkdownTableMode;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-14 01:08:15 +00:00
|
|
|
|
export type HumanDelayConfig = {
|
|
|
|
|
|
/** Delay style for block replies (off|natural|custom). */
|
|
|
|
|
|
mode?: "off" | "natural" | "custom";
|
|
|
|
|
|
/** Minimum delay in milliseconds (default: 800). */
|
|
|
|
|
|
minMs?: number;
|
|
|
|
|
|
/** Maximum delay in milliseconds (default: 2500). */
|
|
|
|
|
|
maxMs?: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export type SessionSendPolicyAction = "allow" | "deny";
|
|
|
|
|
|
export type SessionSendPolicyMatch = {
|
|
|
|
|
|
channel?: string;
|
2026-02-08 16:20:52 -08:00
|
|
|
|
chatType?: ChatType;
|
2026-02-15 02:41:30 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Session key prefix match.
|
|
|
|
|
|
* Note: some consumers match against a normalized key (for example, stripping `agent:<id>:`).
|
|
|
|
|
|
*/
|
2026-01-14 01:08:15 +00:00
|
|
|
|
keyPrefix?: string;
|
2026-02-15 02:41:30 +00:00
|
|
|
|
/** Optional raw session-key prefix match for consumers that normalize session keys. */
|
|
|
|
|
|
rawKeyPrefix?: string;
|
2026-01-14 01:08:15 +00:00
|
|
|
|
};
|
|
|
|
|
|
export type SessionSendPolicyRule = {
|
|
|
|
|
|
action: SessionSendPolicyAction;
|
|
|
|
|
|
match?: SessionSendPolicyMatch;
|
|
|
|
|
|
};
|
|
|
|
|
|
export type SessionSendPolicyConfig = {
|
|
|
|
|
|
default?: SessionSendPolicyAction;
|
|
|
|
|
|
rules?: SessionSendPolicyRule[];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-18 06:37:30 +00:00
|
|
|
|
export type SessionResetMode = "daily" | "idle";
|
|
|
|
|
|
export type SessionResetConfig = {
|
|
|
|
|
|
mode?: SessionResetMode;
|
|
|
|
|
|
/** Local hour (0-23) for the daily reset boundary. */
|
|
|
|
|
|
atHour?: number;
|
|
|
|
|
|
/** Sliding idle window (minutes). When set with daily mode, whichever expires first wins. */
|
|
|
|
|
|
idleMinutes?: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
export type SessionResetByTypeConfig = {
|
2026-02-08 16:20:52 -08:00
|
|
|
|
direct?: SessionResetConfig;
|
|
|
|
|
|
/** @deprecated Use `direct` instead. Kept for backward compatibility. */
|
2026-01-18 06:37:30 +00:00
|
|
|
|
dm?: SessionResetConfig;
|
|
|
|
|
|
group?: SessionResetConfig;
|
|
|
|
|
|
thread?: SessionResetConfig;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-21 16:14:55 +01:00
|
|
|
|
export type SessionThreadBindingsConfig = {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Master switch for thread-bound session routing features.
|
|
|
|
|
|
* Channel/provider keys can override this default.
|
|
|
|
|
|
*/
|
|
|
|
|
|
enabled?: boolean;
|
|
|
|
|
|
/**
|
2026-02-27 10:02:39 +01:00
|
|
|
|
* Inactivity window for thread-bound sessions (hours).
|
|
|
|
|
|
* Session auto-unfocuses after this amount of idle time. Set to 0 to disable. Default: 24.
|
2026-02-21 16:14:55 +01:00
|
|
|
|
*/
|
2026-02-27 10:02:39 +01:00
|
|
|
|
idleHours?: number;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Optional hard max age for thread-bound sessions (hours).
|
|
|
|
|
|
* Session auto-unfocuses once this age is reached even if active. Set to 0 to disable. Default: 0.
|
|
|
|
|
|
*/
|
|
|
|
|
|
maxAgeHours?: number;
|
2026-02-21 16:14:55 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-14 01:08:15 +00:00
|
|
|
|
export type SessionConfig = {
|
|
|
|
|
|
scope?: SessionScope;
|
2026-01-15 10:57:00 +00:00
|
|
|
|
/** DM session scoping (default: "main"). */
|
|
|
|
|
|
dmScope?: DmScope;
|
2026-01-16 14:23:22 -06:00
|
|
|
|
/** Map platform-prefixed identities (e.g. "telegram:123") to canonical DM peers. */
|
|
|
|
|
|
identityLinks?: Record<string, string[]>;
|
2026-01-14 01:08:15 +00:00
|
|
|
|
resetTriggers?: string[];
|
|
|
|
|
|
idleMinutes?: number;
|
2026-01-18 06:37:30 +00:00
|
|
|
|
reset?: SessionResetConfig;
|
|
|
|
|
|
resetByType?: SessionResetByTypeConfig;
|
2026-01-21 13:10:31 -06:00
|
|
|
|
/** Channel-specific reset overrides (e.g. { discord: { mode: "idle", idleMinutes: 10080 } }). */
|
|
|
|
|
|
resetByChannel?: Record<string, SessionResetConfig>;
|
2026-01-14 01:08:15 +00:00
|
|
|
|
store?: string;
|
|
|
|
|
|
typingIntervalSeconds?: number;
|
|
|
|
|
|
typingMode?: TypingMode;
|
2026-02-25 23:53:43 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Max parent transcript token count allowed for thread/session forking.
|
|
|
|
|
|
* If parent totalTokens is above this value, OpenClaw skips parent fork and
|
|
|
|
|
|
* starts a fresh thread session instead. Set to 0 to disable this guard.
|
|
|
|
|
|
*/
|
|
|
|
|
|
parentForkMaxTokens?: number;
|
2026-01-14 01:08:15 +00:00
|
|
|
|
mainKey?: string;
|
|
|
|
|
|
sendPolicy?: SessionSendPolicyConfig;
|
|
|
|
|
|
agentToAgent?: {
|
|
|
|
|
|
/** Max ping-pong turns between requester/target (0–5). Default: 5. */
|
|
|
|
|
|
maxPingPongTurns?: number;
|
|
|
|
|
|
};
|
2026-02-21 16:14:55 +01:00
|
|
|
|
/** Shared defaults for thread-bound session routing across channels/providers. */
|
|
|
|
|
|
threadBindings?: SessionThreadBindingsConfig;
|
fix: unify session maintenance and cron run pruning (#13083)
* fix: prune stale session entries, cap entry count, and rotate sessions.json
The sessions.json file grows unbounded over time. Every heartbeat tick (default: 30m)
triggers multiple full rewrites, and session keys from groups, threads, and DMs
accumulate indefinitely with large embedded objects (skillsSnapshot,
systemPromptReport). At >50MB the synchronous JSON parse blocks the event loop,
causing Telegram webhook timeouts and effectively taking the bot down.
Three mitigations, all running inside saveSessionStoreUnlocked() on every write:
1. Prune stale entries: remove entries with updatedAt older than 30 days
(configurable via session.maintenance.pruneDays in openclaw.json)
2. Cap entry count: keep only the 500 most recently updated entries
(configurable via session.maintenance.maxEntries). Entries without updatedAt
are evicted first.
3. File rotation: if the existing sessions.json exceeds 10MB before a write,
rename it to sessions.json.bak.{timestamp} and keep only the 3 most recent
backups (configurable via session.maintenance.rotateBytes).
All three thresholds are configurable under session.maintenance in openclaw.json
with Zod validation. No env vars.
Existing tests updated to use Date.now() instead of epoch-relative timestamps
(1, 2, 3) that would be incorrectly pruned as stale.
27 new tests covering pruning, capping, rotation, and integration scenarios.
* feat: auto-prune expired cron run sessions (#12289)
Add TTL-based reaper for isolated cron run sessions that accumulate
indefinitely in sessions.json.
New config option:
cron.sessionRetention: string | false (default: '24h')
The reaper runs piggy-backed on the cron timer tick, self-throttled
to sweep at most every 5 minutes. It removes session entries matching
the pattern cron:<jobId>:run:<uuid> whose updatedAt + retention < now.
Design follows the Kubernetes ttlSecondsAfterFinished pattern:
- Sessions are persisted normally (observability/debugging)
- A periodic reaper prunes expired entries
- Configurable retention with sensible default
- Set to false to disable pruning entirely
Files changed:
- src/config/types.cron.ts: Add sessionRetention to CronConfig
- src/config/zod-schema.ts: Add Zod validation for sessionRetention
- src/cron/session-reaper.ts: New reaper module (sweepCronRunSessions)
- src/cron/session-reaper.test.ts: 12 tests covering all paths
- src/cron/service/state.ts: Add cronConfig/sessionStorePath to deps
- src/cron/service/timer.ts: Wire reaper into onTimer tick
- src/gateway/server-cron.ts: Pass config and session store path to deps
Closes #12289
* fix: sweep cron session stores per agent
* docs: add changelog for session maintenance (#13083) (thanks @skyfallsin, @Glucksberg)
* fix: add warn-only session maintenance mode
* fix: warn-only maintenance defaults to active session
* fix: deliver maintenance warnings to active session
* docs: add session maintenance examples
* fix: accept duration and size maintenance thresholds
* refactor: share cron run session key check
* fix: format issues and replace defaultRuntime.warn with console.warn
---------
Co-authored-by: Pradeep Elankumaran <pradeepe@gmail.com>
Co-authored-by: Glucksberg <markuscontasul@gmail.com>
Co-authored-by: max <40643627+quotentiroler@users.noreply.github.com>
Co-authored-by: quotentiroler <max.nussbaumer@maxhealth.tech>
2026-02-09 23:42:35 -05:00
|
|
|
|
/** Automatic session store maintenance (pruning, capping, file rotation). */
|
|
|
|
|
|
maintenance?: SessionMaintenanceConfig;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export type SessionMaintenanceMode = "enforce" | "warn";
|
|
|
|
|
|
|
|
|
|
|
|
export type SessionMaintenanceConfig = {
|
|
|
|
|
|
/** Whether to enforce maintenance or warn only. Default: "warn". */
|
|
|
|
|
|
mode?: SessionMaintenanceMode;
|
|
|
|
|
|
/** Remove session entries older than this duration (e.g. "30d", "12h"). Default: "30d". */
|
|
|
|
|
|
pruneAfter?: string | number;
|
|
|
|
|
|
/** Deprecated. Use pruneAfter instead. */
|
|
|
|
|
|
pruneDays?: number;
|
|
|
|
|
|
/** Maximum number of session entries to keep. Default: 500. */
|
|
|
|
|
|
maxEntries?: number;
|
|
|
|
|
|
/** Rotate sessions.json when it exceeds this size (e.g. "10mb"). Default: 10mb. */
|
|
|
|
|
|
rotateBytes?: number | string;
|
2026-02-23 17:39:48 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Retention for archived reset transcripts (`*.reset.<timestamp>`).
|
|
|
|
|
|
* Set `false` to disable reset-archive cleanup. Default: same as `pruneAfter` (30d).
|
|
|
|
|
|
*/
|
|
|
|
|
|
resetArchiveRetention?: string | number | false;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Optional per-agent sessions-directory disk budget (e.g. "500mb").
|
|
|
|
|
|
* When exceeded, warn (mode=warn) or enforce oldest-first cleanup (mode=enforce).
|
|
|
|
|
|
*/
|
|
|
|
|
|
maxDiskBytes?: number | string;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Target size after disk-budget cleanup (high-water mark), e.g. "400mb".
|
|
|
|
|
|
* Default: 80% of maxDiskBytes.
|
|
|
|
|
|
*/
|
|
|
|
|
|
highWaterBytes?: number | string;
|
2026-01-14 01:08:15 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export type LoggingConfig = {
|
|
|
|
|
|
level?: "silent" | "fatal" | "error" | "warn" | "info" | "debug" | "trace";
|
|
|
|
|
|
file?: string;
|
2026-02-22 17:58:51 +01:00
|
|
|
|
/** Maximum size of a single log file in bytes before writes are suppressed. Default: 500 MB. */
|
|
|
|
|
|
maxFileBytes?: number;
|
2026-01-14 14:31:43 +00:00
|
|
|
|
consoleLevel?: "silent" | "fatal" | "error" | "warn" | "info" | "debug" | "trace";
|
2026-01-14 01:08:15 +00:00
|
|
|
|
consoleStyle?: "pretty" | "compact" | "json";
|
|
|
|
|
|
/** Redact sensitive tokens in tool summaries. Default: "tools". */
|
|
|
|
|
|
redactSensitive?: "off" | "tools";
|
|
|
|
|
|
/** Regex patterns used to redact sensitive tokens (defaults apply when unset). */
|
|
|
|
|
|
redactPatterns?: string[];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-20 18:56:10 +00:00
|
|
|
|
export type DiagnosticsOtelConfig = {
|
|
|
|
|
|
enabled?: boolean;
|
|
|
|
|
|
endpoint?: string;
|
|
|
|
|
|
protocol?: "http/protobuf" | "grpc";
|
|
|
|
|
|
headers?: Record<string, string>;
|
|
|
|
|
|
serviceName?: string;
|
|
|
|
|
|
traces?: boolean;
|
|
|
|
|
|
metrics?: boolean;
|
|
|
|
|
|
logs?: boolean;
|
|
|
|
|
|
/** Trace sample rate (0.0 - 1.0). */
|
|
|
|
|
|
sampleRate?: number;
|
|
|
|
|
|
/** Metric export interval (ms). */
|
|
|
|
|
|
flushIntervalMs?: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-21 10:21:47 +00:00
|
|
|
|
export type DiagnosticsCacheTraceConfig = {
|
|
|
|
|
|
enabled?: boolean;
|
|
|
|
|
|
filePath?: string;
|
|
|
|
|
|
includeMessages?: boolean;
|
|
|
|
|
|
includePrompt?: boolean;
|
|
|
|
|
|
includeSystem?: boolean;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-20 18:56:10 +00:00
|
|
|
|
export type DiagnosticsConfig = {
|
|
|
|
|
|
enabled?: boolean;
|
2026-01-25 10:38:49 +00:00
|
|
|
|
/** Optional ad-hoc diagnostics flags (e.g. "telegram.http"). */
|
|
|
|
|
|
flags?: string[];
|
2026-03-02 00:07:02 +00:00
|
|
|
|
/** Threshold in ms before a processing session logs "stuck session" diagnostics. */
|
|
|
|
|
|
stuckSessionWarnMs?: number;
|
2026-01-20 18:56:10 +00:00
|
|
|
|
otel?: DiagnosticsOtelConfig;
|
2026-01-21 10:21:47 +00:00
|
|
|
|
cacheTrace?: DiagnosticsCacheTraceConfig;
|
2026-01-20 18:56:10 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-14 01:08:15 +00:00
|
|
|
|
export type WebReconnectConfig = {
|
|
|
|
|
|
initialMs?: number;
|
|
|
|
|
|
maxMs?: number;
|
|
|
|
|
|
factor?: number;
|
|
|
|
|
|
jitter?: number;
|
|
|
|
|
|
maxAttempts?: number; // 0 = unlimited
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export type WebConfig = {
|
|
|
|
|
|
/** If false, do not start the WhatsApp web provider. Default: true. */
|
|
|
|
|
|
enabled?: boolean;
|
|
|
|
|
|
heartbeatSeconds?: number;
|
|
|
|
|
|
reconnect?: WebReconnectConfig;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Provider docking: allowlists keyed by provider id (and internal "webchat").
|
2026-01-14 14:31:43 +00:00
|
|
|
|
export type AgentElevatedAllowFromConfig = Partial<Record<string, Array<string | number>>>;
|
2026-01-14 01:08:15 +00:00
|
|
|
|
|
|
|
|
|
|
export type IdentityConfig = {
|
|
|
|
|
|
name?: string;
|
|
|
|
|
|
theme?: string;
|
|
|
|
|
|
emoji?: string;
|
2026-01-22 05:21:47 +00:00
|
|
|
|
/** Avatar image: workspace-relative path, http(s) URL, or data URI. */
|
2026-01-20 14:45:58 -05:00
|
|
|
|
avatar?: string;
|
2026-01-14 01:08:15 +00:00
|
|
|
|
};
|