import { z } from "zod"; /** * Resource monitor configuration. * * The resource monitor runs as a separate OS process (a child of the assistant) * that samples the container's own cgroup memory + workspace disk off the main * event loop, so it keeps recording during a main-thread freeze and its samples * survive an OOM SIGKILL. On a slower timer it also compares each process's * open file-descriptor count against its limit. The assistant spawns it at every * startup: it is platform infrastructure, not an opt-in feature, and there is no * config switch to turn it off. `assistant monitoring start`/`stop` control the * process at runtime only; a stopped monitor respawns on the next boot. */ export const MonitoringConfigSchema = z .object({ sampleIntervalMs: z .number({ error: "monitoring.sampleIntervalMs must be a number" }) .int("monitoring.sampleIntervalMs must be an integer") .min(50, "monitoring.sampleIntervalMs must be at least 50ms") .max(10_000, "monitoring.sampleIntervalMs must be <= 10000ms") .default(250) .describe( "How often the monitor samples memory + disk, in milliseconds. Fast (default 250ms) so vertical memory spikes are caught before an OOM kill.", ), ringBufferSize: z .number({ error: "monitoring.ringBufferSize must be a number" }) .int("monitoring.ringBufferSize must be an integer") .min(100, "monitoring.ringBufferSize must be at least 100") .max(100_000, "monitoring.ringBufferSize must be <= 100000") .default(4000) .describe( "How many recent samples the on-disk ring buffer retains. At the default 250ms interval, 4000 samples is ~16 minutes of history preserved across a crash.", ), highMemThresholdRatio: z .number({ error: "monitoring.highMemThresholdRatio must be a number", }) .min(0.1, "monitoring.highMemThresholdRatio must be >= 0.1") .max(1, "monitoring.highMemThresholdRatio must be <= 1") .default(0.75) .describe( "Fraction of the container memory limit at which a high-memory snapshot (cgroup stats + process tree) is captured to the workspace volume. Default 0.75 (e.g. ~6 GiB of an 8 GiB limit).", ), snapshotCooldownMs: z .number({ error: "monitoring.snapshotCooldownMs must be a number" }) .int("monitoring.snapshotCooldownMs must be an integer") .min(0, "monitoring.snapshotCooldownMs must be non-negative") .default(30_000) .describe( "Minimum interval between high-memory snapshots, in milliseconds, so a sustained spike does not write a snapshot on every sample.", ), fdPollIntervalMs: z .number({ error: "monitoring.fdPollIntervalMs must be a number" }) .int("monitoring.fdPollIntervalMs must be an integer") .min(1_000, "monitoring.fdPollIntervalMs must be at least 1000ms") .max(1_800_000, "monitoring.fdPollIntervalMs must be <= 1800000ms") .default(300_000) .describe( "How often the monitor walks the container's processes to compare their open file-descriptor counts against their limits, in milliseconds. Far slower than the memory sampler (default 5 minutes) because the walk costs a readdir per process and descriptor counts climb over minutes, not milliseconds.", ), highFdThresholdRatio: z .number({ error: "monitoring.highFdThresholdRatio must be a number" }) .min(0.1, "monitoring.highFdThresholdRatio must be >= 0.1") .max(1, "monitoring.highFdThresholdRatio must be <= 1") .default(0.8) .describe( "Fraction of a process's open-file soft limit (RLIMIT_NOFILE) at which the monitor logs a warning identifying the PIDs involved. Default 0.8, leaving headroom before open() starts failing with EMFILE.", ), fdWarnCooldownMs: z .number({ error: "monitoring.fdWarnCooldownMs must be a number" }) .int("monitoring.fdWarnCooldownMs must be an integer") .min(0, "monitoring.fdWarnCooldownMs must be non-negative") .default(60_000) .describe( "Minimum interval between file-descriptor warnings while usage stays over the threshold, in milliseconds. Crossing the threshold always warns immediately; this only throttles the repeats.", ), pluginSourceScanIntervalMs: z .number({ error: "monitoring.pluginSourceScanIntervalMs must be a number", }) .int("monitoring.pluginSourceScanIntervalMs must be an integer") .min(250, "monitoring.pluginSourceScanIntervalMs must be at least 250ms") .max(60_000, "monitoring.pluginSourceScanIntervalMs must be <= 60000ms") .default(2_000) .describe( "How often the plugin source watcher walks the workspace plugin directories and publishes the source-versions sentinel, in milliseconds. Bounds how quickly a plugin source edit becomes visible to live reload.", ), baselineSnapshotIntervalMs: z .number({ error: "monitoring.baselineSnapshotIntervalMs must be a number", }) .int("monitoring.baselineSnapshotIntervalMs must be an integer") .min(60_000, "monitoring.baselineSnapshotIntervalMs must be >= 60000ms") .default(600_000) .describe( "Interval between periodic baseline snapshots, in milliseconds (default 10 minutes). Baselines capture the same forensics as high-memory snapshots but on a steady cadence, so a spike can be diffed against a healthy reference instead of only against other over-threshold captures. Retained separately from high-memory snapshots.", ), }) .describe("Resource monitor process configuration"); export type MonitoringConfig = z.infer;