Skip to main content
Comis writes detailed logs about everything it does — they are your primary tool for understanding what is happening and diagnosing problems. Every message processed, every agent execution, every health check, and every error is recorded.

Where Logs Go

Comis writes logs to two places simultaneously:
  • Log file: ~/.comis/logs/daemon.log — A structured JSON file that is automatically rotated when it gets too large (10 MB by default). The daemon keeps up to 5 rotated copies before deleting the oldest.
  • Console output: The same log data is also written to standard output. This is what you see when using pm2 logs or journalctl.
Logs are in JSON format for machine readability. Each log line is a JSON object with fields like level, msg, module, and timestamps. You typically view them through pm2 or journalctl, which display them line by line.

Log Levels

Comis uses Pino’s standard log levels plus a custom audit level. From most severe to least:
LevelPino ValueWhat It MeansExample
fatal60Unrecoverable process failure, imminent exit. The daemon is about to crash."msg": "FATAL: Bootstrap failed"
error50Something broke and needs attention. Always includes hint and errorKind fields."msg": "Channel connection failed" with "hint": "Check bot token", "errorKind": "auth"
warn40Something is degraded but still working. Also includes hint and errorKind fields."msg": "Gateway token auto-generated (ephemeral)" with "errorKind": "config"
audit35Compliance and security operational events. Custom level between warn and info."msg": "Secret accessed" with "agentId": "assistant"
info30Normal operation boundary events. Startup, shutdown, request completed. This is the default level. Budget: 2-5 lines per request."msg": "Comis daemon started"
debug20Detailed internal operations. Individual LLM calls, tool executions, intermediate steps. Unbounded output."msg": "Tool execution complete" with "toolName": "bash"
trace10Most granular diagnostic level. Available but not used in the codebase by default.(no standard examples — enable for deep protocol debugging)
The silent value exists at the logger API layer to suppress output, but the daemon.logLevels configuration only accepts trace, debug, info, warn, error, and fatal. To quiet a noisy module, raise its level (for example, set it to error) rather than relying on silent.
Start with INFO (the default). Only switch to DEBUG when troubleshooting a specific problem — DEBUG produces significantly more output. Use TRACE only for deep protocol debugging. The FATAL and AUDIT levels are used by the system automatically — you typically do not set them as module log levels.

Viewing Logs

View the last 50 lines (snapshot):
pm2 logs comis --lines 50 --nostream
Follow logs in real time (press Ctrl+C to stop):
pm2 logs comis
pm2 keeps its own log files at ~/.pm2/logs/. The --lines flag shows recent lines from those files, and omitting --nostream follows them live.

Changing Log Levels

Per-Module Overrides

Comis is organized into modules (daemon, agent, channels, scheduler, etc.), and you can set a different log level for each one. This is useful when you want to debug a specific part of the system without being overwhelmed by output from everything else. Add a logLevels section under daemon in your configuration:
# config.yaml
daemon:
  logLevels:
    daemon: "info"       # daemon lifecycle events
    agent: "debug"       # detailed agent execution (LLM calls, tool runs)
    channels: "warn"     # only show channel problems
    scheduler: "info"    # scheduled task events
    skills: "info"       # tool and skill processing
    memory: "info"       # database and embedding operations
    gateway: "info"      # HTTP/WebSocket server events
Each module only logs messages at or above its configured level. In the example above, the agent module logs everything including DEBUG messages, while the channels module only logs warnings and errors.
The most common use case is setting agent: "debug" to see exactly what your agent is doing during a conversation — which tools it calls, how long each LLM request takes, and what context it retrieves from memory.

Runtime Level Changes

You can change log levels without restarting the daemon by using the daemon.setLogLevel RPC command. This is useful for temporarily enabling DEBUG logging to investigate an issue, then switching back to INFO when you are done.
# Enable debug logging for the agent module
curl -X POST http://localhost:4766/rpc \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"method":"daemon.setLogLevel","params":{"module":"agent","level":"debug"}}'
Runtime level changes take effect immediately and persist until the daemon restarts, at which point the levels revert to whatever is in your configuration file.

Error Classification

Every ERROR and WARN log entry includes two mandatory fields that help you quickly understand and act on problems:
  • hint — A human-readable string explaining what to do about the error
  • errorKind — A classification tag from a fixed set of 9 categories

ErrorKind Values

KindDescriptionTypical Action
configConfiguration parsing, missing keys, schema violationsCheck config.yaml against the Config Reference
networkTCP/HTTP failures, DNS resolution, connection resetsCheck network connectivity and firewall rules
authAuthentication or authorization failures (401/403, bad token)Verify API keys and tokens in SecretManager
validationInput validation failures (bad request body, invalid params)Check the request payload format
timeoutOperation exceeded deadline (LLM call, HTTP request, DB query)Increase timeout or check provider status
resourceResource exhaustion (OOM, disk full, file descriptor limit)Free resources or increase limits
dependencyExternal service unavailable (LLM provider, embedding API)Check provider status pages and retry
internalUnexpected internal errors (assertion failures, logic bugs)Report as a bug with the full log entry
platformChat platform API errors (Discord, Telegram, Slack rate limits)Check platform status and rate limit headers
Filter logs by errorKind to quickly find related problems. For example, to see all authentication errors: grep '"errorKind":"auth"' ~/.comis/logs/daemon.log

Memory and Recall Signals

The memory module (set its level under daemon.logLevels.memory, shown above) emits two families of structured signal: step-tagged write/curation logs and graceful-degradation WARNs.

Step-tagged write and curation logs

Memory writes and the background curation jobs carry a step tag plus a durationMs timing field, so you can filter the memory pipeline by stage:
stepStage
extractExtracting candidate memories from a turn
storePersisting a memory entry (also surfaces as the INFO “Memory store complete” forensic event)
linkResolving and linking entities during a memory-review pass
consolidateClustering and consolidating memories into observations
# Follow the memory write/curation pipeline by stage
jq 'select(.step == "consolidate")' ~/.comis/logs/daemon.log
The aggregate outcome of these passes is also emitted as the typed memory:entities_linked and memory:consolidated events — see Observability → Memory & Recall Diagnostics.

Graceful-degradation WARNs (no silent degradation)

Recall degrades gracefully, and every degradation path logs a WARN carrying the mandatory errorKind + hint pair (see Error Classification above) — recall never drops to a cheaper path silently:
DegradationWARN signal
Reranker unavailable / timed out → fusion-ranked fallbackWARN with errorKind + hint; corresponds to memory:reranked.fellBack / .timedOut
Vector index unavailable → FTS-only recallWARN with errorKind + hint; corresponds to memory:recalled.vectorCandidates: 0
Candidate missing embedding, or invalid extraction/consolidationWARN with errorKind + hint
As with all Comis logs, these never log secrets, message bodies, or absolute paths — the per-recall ranking detail lives only in the opt-in, full-sanitized recall-trace artifact, never in daemon.log.
# Spot recall falling back to a cheaper path
grep '"errorKind"' ~/.comis/logs/daemon.log | grep -i rerank
For the full “why did recall pick X / why is recall slow?” diagnosis flow, see the Troubleshooting runbook.

Field Dictionary

Every log entry is a JSON object. Beyond the standard Pino fields (level, time, msg, pid, hostname), Comis uses 48 canonical fields organized by category. These are the fields you will see when reading JSON log output.

Core Identity

FieldTypeWhen Present
agentIdstringAgent-scoped operations
traceIdstringAuto-injected via AsyncLocalStorage on every request
channelTypestringChannel adapter logs (e.g., “telegram”, “discord”)
modulestringAlways — set via logLevelManager.getLogger("module")

Timing

FieldTypeWhen Present
durationMsnumberAny timed operation (LLM calls, tool execution, startup)
startupDurationMsnumberDaemon startup complete log
shutdownDurationMsnumberDaemon shutdown complete log
connectionDurationMsnumberWebSocket connection lifetime (on close)

Operation

FieldTypeWhen Present
toolNamestringTool/skill execution logs
methodstringRPC/HTTP method name
requestIdstringPer-HTTP-request correlation ID

Error

FieldTypeWhen Present
errobjectError objects. Use err, not error — matches Pino serializer.
hintstringRequired on ERROR and WARN. Actionable guidance for the operator.
errorKindErrorKindRequired on ERROR and WARN. One of 9 classification values.

Token and Cost

FieldTypeWhen Present
tokensInnumberAfter LLM call — input token count
tokensOutnumberAfter LLM call — output token count
tokensTotalnumberAfter LLM call — total token count
cacheReadTokensnumberAfter LLM call — tokens served from prompt cache
cacheCreationTokensnumberAfter LLM call — tokens written to prompt cache
estimatedCostUsdnumberAfter LLM call — estimated cost in USD

Pipeline

FieldTypeWhen Present
stepstringPipeline step name during multi-step processing
reasonstringWhy an action was taken (e.g., compaction trigger reason)
inputLennumberInput length for processing steps
outputLennumberOutput length for processing steps
itemCountnumberCount of items processed
successbooleanWhether a processing step succeeded

WebSocket

FieldTypeWhen Present
activeConnectionsnumberWebSocket connection events
closeCodenumberWebSocket close events
closeReasonstringWebSocket close events
closeTypestringWebSocket close classification
connectionIdstringWebSocket connection identifier
clientIdstringWebSocket client identifier

Message

FieldTypeWhen Present
messageTruncatedbooleanWhen a message was truncated before delivery
messageLennumberLength of incoming message
responseLennumberLength of outgoing response

Agent Execution

FieldTypeWhen Present
llmCallsnumberExecution complete summary — total LLM API calls
toolCallsnumberExecution complete summary — total tool invocations
prunedMessagesnumberAfter context compaction — messages removed
sessionMessageCountnumberCurrent session message total
stopReasonstringWhy execution ended (e.g., “end_turn”, “max_tokens”, “tool_use”)

System

FieldTypeWhen Present
instanceIdstringDaemon instance identifier (unique per process)
configNamestringConfig file being loaded
shutdownOrdernumberComponent shutdown sequence number

Truncation Flags

FieldTypeWhen Present
paramsTruncatedbooleanWhen RPC parameters were truncated in log output
queryTruncatedbooleanWhen a database query was truncated in log output

Security

Comis automatically redacts sensitive information from log output. API keys, tokens, passwords, and other credentials are replaced with [REDACTED] before being written to the log file or console. The redaction engine covers all credential field name patterns across multiple nesting depths (top-level through 3 levels of nesting):
Redacted Fields
apiKey, token, password, secret
authorization, accessToken, refreshToken, botToken
privateKey, credential, credentials, key
passphrase, connectionString, accessKey
cookie, webhookSecret
Nested paths are also redacted: config.telegram.botToken, *.*.apiKey, etc., up to 4 levels deep (*.*.*.*).
You do not need to worry about sensitive data in logs. Comis automatically detects and hides API keys, tokens, and passwords using a compiled redaction engine. The redaction rules are compiled once at startup for zero runtime overhead.

Log Rotation

Comis automatically rotates all observability log streams to bound disk usage.

observability.logRotation — Cross-Stream Policy

A single policy governs five log streams under ~/.comis/logs/:
  • daemon.log — daemon-wide structured JSON log
  • cache-trace.jsonl — per-turn cache observability (cache hits, misses, tokens)
  • config-audit.jsonl — config-write audit trail (all writes to config.yaml)
  • session-index.YYYY-MM-DD.jsonl — append-only session index (date-rolled, one file per UTC day)
  • *.trajectory.jsonl — per-session trajectory recordings (one file per agent session)
Configure the policy under observability.logRotation in your config.yaml:
# config.yaml
observability:
  logRotation:
    maxSizeBytes: 52428800   # 50 MB; pino-roll rolls when daemon.log crosses this
    maxFiles: 5              # keep 5 rotated copies per stream (oldest deleted)
    maxAgeDays: 30           # delete rotated files older than 30 days
    compressAged: true       # gzip rotated files (.log.gz / .jsonl.gz)
FieldDefaultWhat It Controls
maxSizeBytes52428800 (50 MB)Size at which daemon.log rolls. Honored by pino-roll for daemon.log and by per-stream caps for the JSONL streams.
maxFiles5Number of rotated copies retained per stream. Oldest deleted on rotation or startup sweep.
maxAgeDays30Files older than this are deleted on the daemon-startup sweep (mtime-based).
compressAgedtrueGzip rotated files after rename — saves ~5× disk space on JSON text.

How to Verify

Inspect the resolved policy, including any operator overrides, with:
comis config get observability.logRotation

Storage Budget

Worst-case storage: 5 streams × 5 files × 50 MB = 1.25 GB. With compressAged: true (the default), gzipped JSON text compresses approximately 5×, so realistic steady-state is ~300 MB. Increase maxFiles or maxAgeDays if you need more history; reduce them on tight disks.

Per-Stream Behavior

daemon.log — pino-roll handles size-based rotation in a worker thread; rotated files become daemon.1.log, daemon.2.log, etc. The daemon-startup sweep gzips any uncompressed rotated files and removes those beyond maxFiles or older than maxAgeDays. cache-trace.jsonl — a per-file cap (50 MB) prevents a single cache-trace file from growing unbounded. The startup sweep prunes accumulated history against maxFiles and maxAgeDays. config-audit.jsonl — a small (10 MB) rename-shift rotation runs on every append, keeping the active file write-friendly. The startup sweep gzips the resulting rotated copies and prunes them by count and age. session-index.YYYY-MM-DD.jsonl — a new file starts each UTC day (date-roll). Old dated files are pruned by the startup sweep against maxFiles and maxAgeDays. Only today’s dated file is treated as the active base and is never pruned. *.trajectory.jsonl — one file per agent session, with a 10 MB soft cap and a 50 MB hard cap applied in-session. Accumulated session files across past sessions are pruned by the startup sweep against maxFiles and maxAgeDays.

Legacy daemon.logging Keys (Backward Compatibility)

The legacy daemon.logging.maxSize and daemon.logging.maxFiles keys (documented below) continue to work for daemon.log specifically. When both are set, observability.logRotation takes precedence for the cross-stream sweep, and daemon.logging.maxSize retains its role as the pino-roll size threshold if you have not yet migrated to the new policy.
Operators are encouraged to migrate to observability.logRotation for a unified, consistent rotation policy across all five log streams. The legacy daemon.logging keys remain supported for backward compatibility but govern only daemon.log.

Legacy daemon.logging Configuration

You can still customize per-file daemon-log rotation using the legacy keys:
# config.yaml
daemon:
  logging:
    filePath: "~/.comis/logs/daemon.log"   # where to write the active log file
    maxSize: "10m"                     # rotate when file reaches 10 MB
    maxFiles: 5                        # keep up to 5 rotated copies
SettingDefaultWhat It Controls
filePath~/.comis/logs/daemon.logPath to the active log file. Supports ~ expansion.
maxSize10mMaximum file size before rotation. Supports k, m, g suffixes. When observability.logRotation.maxSizeBytes is set, it overrides this threshold for pino-roll.
maxFiles5Number of rotated files to keep. Older files are deleted.
With the new cross-stream policy active (the default), observability.logRotation controls the 50 MB roll threshold and the 5-file / 30-day retention for all streams. The daemon.logging.maxSize value still reaches pino-roll as the size threshold if you have not set observability.logRotation.maxSizeBytes.

Trace Files

In addition to the main log file, Comis can write per-agent JSONL trace files that capture detailed execution data. These are useful for debugging specific agent conversations.
# config.yaml
daemon:
  logging:
    tracing:
      outputDir: "~/.comis/traces"   # directory for trace files
      maxSize: "5m"                   # rotate trace files at 5 MB
      maxFiles: 3                     # keep 3 rotated trace files per session
Trace files are created automatically when an agent processes messages. They contain the full execution timeline: prompts sent, responses received, tools called, and timing data.

Daemon

How the daemon starts, runs, and shuts down.

Monitoring

Health checks that watch your system’s vital signs.

Observability

Token tracking, cost estimation, and performance data.

Troubleshooting

Common problems and how to solve them.