Skip to main content
What this is for: how config and skill changes propagate without a manual restart, and which kinds of changes still require one. Who it’s for: operators tuning a live deployment and developers building skills or automating config updates. Comis has two distinct hot reload mechanisms: config hot reload uses RPC-based writes followed by a SIGUSR1-triggered process restart, while skill hot reload uses chokidar file watchers for discovery directory monitoring. These are completely separate systems.

What reloads, and how

ChangeHow it appliesDrops connections?
config.patch / config.apply (any section)Validate → write → 200 ms delayed SIGUSR1 → process restartYes — supervisor (pm2/systemd) restarts the daemon. WS clients reconnect with restart-continuation.
agents.create / agents.update / agents.deleteSame path, but with a 2-second debounced SIGUSR1 so batches coalesceYes
tokens.create / tokens.revoke / tokens.rotateSame debounced restart pathYes
channels.enable / channels.disableSame debounced restart pathYes
gateway.restart RPCDirect SIGUSR1 (200 ms delay to flush response)Yes
tooling.* (entire subtree)config.patch / config.apply validate → write → 200 ms delayed SIGUSR1 → process restart. Cannot hot-flip within a running session — tooling.capabilityIndex.enabled selects between two cached system-prompt shapes (one-line residual vs flat tool dump), and tooling.installDetours.mode is read at adapter-construction time. Operator-only: agents cannot self-edit (the entire tooling subtree is in IMMUTABLE_CONFIG_PREFIXES).Yes
New SKILL.md added to a discovery pathchokidar add event → debounced re-discovery, no restartNo
Existing SKILL.md edited or deletedchokidar change / unlink event → debounced re-discoveryNo
Editing ~/.comis/config.yaml directlyNot detected. Run comis config validate then restart manually.
The takeaway: everything except skill files goes through a daemon restart. The restart is graceful (ordered teardown, ~30 s hard timeout) and active sessions are preserved across the bounce via the restart continuation tracker, so end users typically don’t notice.
Config hot reload is NOT file watching. The daemon does not watch config files for changes. Config changes go through the RPC API, which validates, writes, and then sends SIGUSR1 to restart the daemon.

Config Hot Reload

Config changes are applied through RPC methods that validate the change, write to the local YAML file, and trigger a SIGUSR1 signal for process restart.
Source: packages/daemon/src/rpc/config-handlers.ts — 10 RPC methods including config.patch, config.apply, config.rollback, gateway.restart.

config.patch

Deep-merges a partial config change into the existing configuration. Flow:
  1. Rate limit check — token bucket, 5 patches per 60 seconds (shared with config.apply)
  2. Immutable path check — certain config paths cannot be modified at runtime
  3. Build patch object — supports dot-notation keys (e.g., budget.maxTokens)
  4. Type coercion — automatically converts string booleans ("true" / "false") and numeric strings to native types (handles LLM tool call quirks)
  5. Deep merge — merge patch into current in-memory config
  6. Zod validation — validate merged result against AppConfigSchema
  7. MCP server env restore — restore env fields from existing YAML when the UI patch omits them (because config.read redacts secret values)
  8. Duplicate MCP server name check — reject patches with duplicate server names
  9. Suspicious env value check — reject bare $VAR, [REDACTED], raw keys
  10. Atomic write — write to temp file, then rename() to config.local.yaml
  11. Best-effort git commit — record the change in config git history
  12. Audit event — emit audit:event with actionType: "config.patch"
  13. 200ms delayed SIGUSR1 — schedule process restart to pick up new config

config.apply

Full section replacement (not deep merge). Same flow as config.patch except step 5 replaces the entire section instead of merging. Shares the rate limit bucket with config.patch.

config.rollback

Restores config from git history to a specific commit SHA. Calls configGitManager.rollback(sha), then sends SIGUSR1 for restart.

persistToConfig (Internal Utility)

Used by management RPC handlers (agents, tokens, channels) to persist config changes. Follows the same validate-write-restart pattern but with a 2-second debounced SIGUSR1 instead of the 200ms delay used by config.patch. This allows batch operations (e.g., creating 8 agents in sequence) to coalesce into a single restart.
Source: packages/daemon/src/rpc/persist-to-config.ts — debounced SIGUSR1 for batch config operations.

Immutable Config Keys

Certain config paths cannot be modified at runtime through the RPC API. Attempting to patch these paths returns an error instructing the operator to modify config files manually.

Git Versioning

Config changes are recorded in a git repository for history and rollback. The git operations are best-effort: if git is unavailable or the commit fails, the config change still proceeds. History and diff operations are available through the config.history and config.diff RPC methods.

Config Patch Rate Limiting

A token bucket rate limiter enforces a maximum of 5 patches per 60 seconds. This limit is shared between config.patch and config.apply to prevent runaway config changes from agent tool calls or automated scripts. When the rate limit is exceeded, the RPC returns an error with a retry-after duration:
Config patch rate limit exceeded: max 5 patches per minute.
Try again in N seconds.

Skill Hot Reload

Skill hot reload uses chokidar file watchers to monitor discovery directories for SKILL.md file changes. This is a completely separate mechanism from config hot reload.
Source: packages/skills/src/registry/skill-watcher.ts — chokidar-based file watcher with batch debounce.

createSkillWatcher

function createSkillWatcher(options: SkillWatcherOptions): SkillWatcherHandle
Options:
FieldTypeDescription
discoveryPathsstring[]Directories to watch for skill file changes
debounceMsnumberBatch debounce interval in milliseconds
onReload() => voidCallback fired after debounce window closes
loggerSkillsLogger?Optional logger for diagnostic output

Watched Events

The watcher monitors for three chokidar events on discovery directories:
EventTrigger
addNew SKILL.md file created
changeExisting SKILL.md file modified
unlinkSKILL.md file deleted

Batch Debounce

Rapid file changes within the debounceMs window are coalesced into a single onReload() invocation. This prevents redundant re-discovery when multiple skill files are modified simultaneously (e.g., during a git checkout or bulk file operation).

Late Directory Creation

When discovery paths do not exist yet (e.g., the workspace skills/ directory has not been created by an agent), the watcher monitors their parent directories for creation:
  1. For each missing discovery path, walk up to the nearest existing ancestor directory
  2. Watch those ancestor directories with chokidar at depth 3
  3. When a missing discovery path appears (detected via addDir event):
    • Close the parent watcher (its job is done)
    • Start the real skill file watcher on all now-existing discovery paths
    • Trigger re-discovery for newly available paths

Lifecycle

The SkillWatcherHandle provides a close() method for shutdown cleanup. During graceful shutdown, all skill watchers are closed in the teardown sequence.

Gateway Restart

The gateway.restart RPC method triggers the same SIGUSR1 mechanism as config hot reload:
// 200ms delay allows the RPC response to flush over WebSocket
setTimeout(() => {
  process.kill(process.pid, "SIGUSR1");
}, 200);
This is used when the gateway needs to restart for reasons other than config changes (e.g., TLS certificate rotation).

SIGUSR1 Lifecycle

When SIGUSR1 is received, the daemon initiates an ordered teardown sequence followed by process exit. The process manager (pm2 or systemd) detects the exit and restarts the daemon with the updated config.
Source: packages/daemon/src/wiring/setup-shutdown.ts — ordered teardown with hard timeout.

Teardown Sequence

The shutdown handler stops components in dependency order:
  1. Graph coordinator (cancel running graph pipelines)
  2. Sub-agent runner (drain active sub-agent runs)
  3. Lock cleanup timer
  4. Approval gate (dispose pending timers)
  5. Skill file watchers (close chokidar instances)
  6. Cron schedulers (per-agent)
  7. Session reset schedulers (per-agent)
  8. Browser services (close Chrome processes)
  9. Restart continuation tracker (capture active sessions)
  10. Lifecycle reactors
  11. Channel manager (stop all channel adapters)
  12. Heartbeat runner
  13. Per-agent heartbeat runner
  14. Wake coalescer
  15. Media temp manager
  16. Gateway HTTP/WebSocket server
  17. Observability modules (diagnostic collector, activity tracker, delivery tracer)
  18. Background embedding indexing (with 5-second timeout)
  19. Audit aggregator
  20. Injection rate limiter
  21. Secret store database
  22. Memory database

Hard Timeout

The shutdown handler has a hard timeout of 30 seconds (default timeoutMs). If the teardown sequence does not complete within this window, the process is forcibly terminated. This timeout must be less than the process manager’s stop timeout (e.g., systemd TimeoutStopSec).

Signal Registration

process.on("SIGUSR1", () => {
  daemonLogger.info("SIGUSR1 received, initiating restart");
  void shutdownHandle.trigger("SIGUSR1");
});
The SIGUSR1 handler triggers the same graceful shutdown used for SIGTERM/SIGINT, ensuring all components are properly cleaned up regardless of the restart trigger.

Security Model

Defense-in-depth security architecture

Rate Limiting

Multi-layer rate limiting

Config YAML

Configuration file reference