Troubleshooting
Sintoma → diagnostic → fix. Top-down by where the problem lives.
The decision tree:
When in doubt, start with dotagent doctor + dotagent status — they're read-only and give the most signal per second.
Daemon won't start
Symptom: dotagent reload says "daemon not running"
ls -l ~/.config/dotagent/state/daemon.pidFile missing → daemon was never started (or was bootouted). Start it:
macOS:
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/run.avelino.dotagent.plistLinux:
systemctl --user enable --now run.avelino.dotagent
File present but
ps -p $(cat …)returns nothing → stale pidfile from a crashed daemon. Delete it and start again:
Symptom: launchd / systemd loads the unit but the daemon dies immediately
This is exactly what the captured stderr file is for — a daemon that dies before or during startup has nowhere else to say why:
Only crashes land there. Under launchd / systemd the daemon does not mirror its
tracingstream to stderr (stderr is an unrotated file there, and the stream already goes todotagent.log). So an empty…-error.logon a daemon that keeps dying means it is not crashing at all — checklogs/daemon/dotagent.loganddotagent statusinstead. To force the mirror on anyway, setDOTAGENT_LOG_STDERR=1in the unit's environment.
Common causes:
telemetry init failed: ...
Bad [telemetry] config. Either fix config.toml or delete the section.
config.toml parse error: ...
TOML syntax issue. Validate with toml-cli get config.toml . or remove the file.
No such file or directory: dotagent
The unit's ExecStart points at a binary that was moved. Re-run dotagent install.
permission denied
The dotagent binary lost the executable bit. chmod +x it.
Symptom: macOS — unit loads but launchctl print shows state = exited
ThrottleInterval (10s) is in play — launchd waits 10s before respawning. After many crashes in quick succession, launchd will back off further. The real diagnosis is in the error log above.
Symptom: Linux — systemctl --user status shows Active: inactive (dead) despite enable --now
If you see User lingering not enabled, the user session is killed at logout. Enable lingering:
Doctor errors
agent.name is empty
The [agent].name field is missing or "". Open the manifest and add it.
run.command is empty
[run].command field missing. dotagent needs the command to invoke (e.g., "fish", "python3", "./agent").
duplicate schedule id: <id>
Two [[schedules]] blocks have the same id. Schedule ids must be unique within a single manifest — across different manifests they can repeat.
✗ plugin <name> not found
A [[preflight]], [[on_success]], [[on_failure]], or [[notifiers]] driver = "plugin" references a plugin binary that isn't on $PATH.
If absent on all four:
Brew install:
brew install dotagent(when tap publishes) ships every first-party plugin.Cargo install:
cargo install --path plugins/sink-roam(or whichever).Release binaries: re-extract the tarball into a
$PATHdirectory.
⚠ <name>: no [security] section
Warning, not error. v0 is schema-only. Add a minimal block to silence it:
⚠ <name>: manifest drift since last daemon run
The on-disk manifest's sha256 doesn't match what's cached in state/known_manifests.json from the last daemon load. Cause is one of:
You edited it intentionally. Run
dotagent reload. The new hash gets cached.You DIDN'T edit it. Drift detection is doing its job — investigate. Compare timestamps:
Agent never runs
Symptom: dotagent doctor says manifest is fine but the daemon never fires the agent
Start with the dry-run:
If dispatched = 0 and you expected dispatch:
The current window has already succeeded — heartbeat says
last_success_at >= expected_at. Rundotagent inspect <name>to confirm.The agent's
monitor = falseexcludes it from the daemon. Setmonitor = true(or remove the line — default is true).The schedule's window is in the future.
next eventshows when.
Symptom: monitor = false agents fire when I don't want them to
monitor = false excludes the agent from tick / daemon dispatch, but NOT from run / run-now. The example hello-* agents use this so they only run when you explicitly invoke them.
Symptom: agent fires but with the wrong args
AGENT_ARGV is the schedule's args, NOT [run].args. The full argv the agent receives is [run].command + [run].args + schedule.args.
The Python script is invoked as python3 ./agent.py --mode prod --period last-week. Inside the script, AGENT_ARGV = ["--period","last-week"].
Symptom: schedule type "interval" doesn't fire on a fresh install
Interval schedules with no previous run start "from now". A interval_minutes = 60 agent installed at 14:32 next fires at 15:32. Set a smaller interval (or dotagent run-now) to verify.
Symptom: cron-style schedule never matches
weekdays: 0 = Sunday, 6 = Saturday (matches launchd Weekday). Many folks expect 0 = Monday — that's cron(8), not us. dotagent matches launchd.
Quick sanity:
Agent runs but fails
Symptom: exit_code = 124
The agent was killed for exceeding agent.timeout_seconds. dotagent sends SIGTERM, waits 5 seconds, then SIGKILL. Either:
Bump
timeout_secondsin the manifest.Fix the agent (most timeouts are an external CLI hanging — wrap with
--timeout).
timeout_seconds is the backstop, not the first line of defense. It kills a wedged run; it does nothing to stop one slow network call from eating the whole budget and leaving the agent no time to finish the work it already did. Put a ceiling on the call itself — curl --max-time 20, an HTTP client timeout, timeout 30 <cmd> — so a single unresponsive endpoint degrades one step instead of the run.
Symptom: agent succeeded once but the daemon keeps re-firing it
The previous run failed and you fixed it without updating state. The window doesn't yet show succeeded_at. Run dotagent run-now <name> to force a fresh success, OR delete the window file:
The daemon will recompute on the next tick.
Symptom: dotagent run works but the agent fails when the daemon fires it
Almost always an environment difference. The interactive shell has $PATH / $HOME / $LANG you take for granted; the daemon inherits launchd / systemd's much-poorer env.
Check what the daemon sees:
Set explicit $PATH in the agent's manifest:
Or inherit = true plus tighten the unit-level env vars in the plist.
Symptom: preflight aborts the run but I want to see why
The suggest field is the message the plugin returned ("warp-cli connect", etc.).
To run the preflight manually:
Notifier / sink not working
Desktop notifier doesn't show banners (macOS)
System Settings → Notifications → make sure the terminal app running the daemon (and possibly dotagent itself) is allowed. notify-rust uses NSUserNotification, which inherits the parent process's notification entitlements.
For launchd-managed daemons, the entitlement lives with the daemon binary itself — TCC may show dotagent as the requestor first time.
Desktop notifier doesn't show banners (Linux)
notify-rust uses D-Bus. Verify the daemon process can talk to your notification daemon:
If notify-send shows nothing, no D-Bus session is reachable. Common causes:
Headless / SSH session with no
$DBUS_SESSION_BUS_ADDRESS.systemd user unit started before the graphical target — set
Wants=graphical-session.targetin the unit.
iMessage notifier doesn't send
imessage runs osascript to talk to Messages.app. Verify manually:
If that fails, automation isn't permitted: System Settings → Privacy & Security → Automation → Terminal (or dotagent) → toggle Messages on.
The imessage driver also rate-limits via $DOTAGENT_HOME/state/notify/imessage/<slug>.json. If you're sending within rate_limit_minutes of a previous successful send, the call is skipped (not failed). Look for it in the audit log:
Slack / ntfy / Pushover / Telegram notifier doesn't fire
Native HTTPS — check connectivity from the daemon's environment:
If that succeeds but dotagent's notifier doesn't, check the audit log for a Backend(...) error:
Three causes account for most of it:
<field>: env var ${X} is unset. A ${VAR} in a credential field did not resolve. dotagent fails the send rather than posting the literal placeholder, so nothing arrives. Check that the key is in ~/.config/dotagent/secrets.env, that the file is mode 0600 (otherwise the daemon refuses the whole file), and that you sent SIGHUP after editing it — the store is read at startup and on reload, not per send.
<driver> transport error (...). Network or TLS. The message carries a kind and, when there was one, an HTTP status — deliberately not the URL, because for Slack and Telegram the URL is the credential. Reproduce with the curl above from the daemon's own environment (a proxy set in your shell but not in the plist is a classic).
Nothing arrived, no error. Check the event filter first — events = ["given_up"] on an agent that keeps recovering never fires. Then check size: an over-limit body is now trimmed with a [truncated] marker rather than rejected, so if you see a truncated message that is the cap doing its job (ntfy counts bytes, not characters — see notifications).
For Telegram specifically, an API refusal now carries the Bot API's own description ("Bad Request: message is too long", "chat not found"), which is usually the whole answer.
Sink plugin appears not to run
Sinks fire only on agent exit 0. If the agent failed, only the notifier path runs.
sink-roam writes but the previous block doesn't get replaced
Your marker_regex doesn't match the old root block. See plugins/sink-roam.md.
A manifest is broken
One agent stopped running and nothing said why
Check for a parse failure:
A manifest that fails to parse or validate is skipped, audited as manifest_invalid (Critical, so it fires out-of-band notification), and the other agents keep running.
Before this behavior existed, one broken file aborted the entire scan: the daemon saw zero agents, dispatched nothing, and the only trace was a single
warn!line. If you are on an older build and everything stopped at once, look for a manifest you edited recently.
Inbound Telegram
The bot never answers
Check whether the ingress even started:
telegram ingress: OFF — bot_token set but allowed_user_ids is empty is the most common cause. An empty allowlist means nobody, never everybody. Add your numeric user id (from @userinfobot) — a @username will not work, it is not an authorization input.
If the ingress is on, your message may be getting refused:
reason: "user id not in allowed_user_ids" means the id in config.toml is not the one that sent the message. reason: "rate limit exceeded" means you passed rate_limit_per_minute (default 10).
telegram poll failed repeating in the log
Transport problem — network down, or a revoked/wrong token. The backoff doubles to a 60-second ceiling, so this will not spin. Verify the token resolves:
An unresolved ${TELEGRAM_BOT_TOKEN} shows up there as an unresolved reference.
The run happens but no reply arrives
The dispatcher printed nothing on stdout, or logged to stdout instead of stderr and the agent then failed. stdout is the reply:
The reply is cut off mid-sentence
Telegram caps a message at 4096 characters. Longer output is trimmed with a [truncated] marker; the full text is in dotagent logs.
dispatcher agent '<name>' not found
doctor says this when dispatcher_agent in config.toml does not match any installed agent. Every accepted message would fail after passing the allowlist.
MCP server
tools/list returns an error instead of tools
One or more agent.toml files failed to load. The healthy agents are fine — the daemon still runs them — but the MCP catalog refuses to be served incomplete, because a model handed a list that quietly lost entries would answer confidently and wrongly about what it can do.
The error names each failing path. dotagent doctor shows the same list with the parse messages.
An agent is missing from tools/list
Two agent names can sanitize to the same tool name (a.b and a/b both become run-a-b). The first one discovered wins and the log says:
Rename one of them.
Runs started via MCP don't show in dotagent status
Expected. dotagent mcp runs agents in its own process, like run-now; status reflects only the daemon's supervisor. Their heartbeats live under the trigger-mcp slug, visible via dotagent inspect.
Logs
dotagent logs <name> says "no logs found"
The agent has never run, or the log directory was deleted. Run:
Logs are noisy / full of tracing debug spam
Or transient:
Disk filling despite retention being set
retention_daysinconfig.tomlonly kicks in during the 03:00 sweep. If the daemon hasn't been alive at 03:00 since the last rollover, nothing got swept yet.The sweep needs write permission on
logs/agents/<name>/. If your agent script chowns its log directory (don't), the sweep fails silently.The audit log is never swept. It rotates at 32MB into
audit.log.<YYYYMMDDTHHMMSS>segments, and those segments stay forever — it is the only forensic artifact dotagent keeps, so pruning it is your call, not a sweeper's. Plan for ~1KB per event, roughly a hundred events/day.state/windows/is swept by the same 03:00 pass, on[state] window_retention_days(default 30). A 15-minute agent writes ~96 files a day there, so a daemon that never sees 03:00 accumulates thousands. A window whose.lockis currently held is skipped — it gets collected on the next pass.
Audit log
Ask the log what it can still prove
Start here for anything in this section. It prints one of four verdicts — intact from GENESIS, intact since a named rotation, unexplained truncation, or broken at a position — and exits non-zero for the last two. --json for scripts. Without --full it checks only the live audit.log, which is what the daemon does at boot.
Full output reference: reference/cli.md.
audit_chain_broken event in the log
The daemon detected tampering (or a partial write) of audit.log. The event is itself a chained entry — investigation:
position is the line number where the chain broke. Read context:
Cause is almost always:
Someone (or you) edited the file by hand.
A crash mid-write left a half-line, or bytes cut inside a multibyte character. dotagent steps over that garbage on the next append rather than refusing to write — the line stays on disk and verification keeps reporting it, which is the loud half of the trade.
Disk corruption.
The head of the file was removed — see the next section, which is the one case that is easy to cause by accident.
dotagent continues operating — the new chain is anchored to the broken position. Forensics is on you; dotagent audit verify --full names the position and the segment.
position: 0 with expected_prev_hash: "GENESIS"
The first line of audit.log does not chain to GENESIS, and no seam explains why. Verification calls this unexplained truncation: somebody removed the head of the log.
The legitimate way for the file to start somewhere other than GENESIS is rotation, which leaves a seam as line 1:
If that line is missing, someone sed'd or tail'd the file. Deleting a whole segment is fine and stays quiet — the seam lives in the current file and keeps explaining the gap. Deleting lines from the current file takes the seam with them, and that is what fires here.
Rotated segments
audit.log is live; audit.log.<YYYYMMDDTHHMMSS> are sealed segments, oldest first by name. To read the whole history in order:
Removing old segments is supported and does not break verification — it reports "intact since <ts>" instead of "intact from GENESIS". The reasoning is in security/threat-model.md.
Plugin protocol
Plugin always returns ok=false
Stderr has the human-readable error. Stdout has the JSON response.
Plugin works manually but not from the daemon
Cause is almost always environment:
$PATHdifferences (covered in Agent runs but fails).$DOTAGENT_PLUGIN_PATHnot inherited by launchd. Set it in the plist'sEnvironmentVariables.HOME differences (the plugin reads
~/.config/<x>but the daemon's HOME is somewhere unexpected).
Plugin info JSON is malformed — dotagent doctor won't parse it
Run the plugin directly:
If jq errors out, the plugin is printing log lines to stdout (forbidden — stdout is reserved for JSON). Patch the plugin to use stderr for logs.
Performance
Daemon CPU usage is high
The daemon should be near-zero CPU outside of a tick. If top/htop shows persistent CPU:
A plugin is spinning.
dotagent plugin listthen check each.A schedule with
interval_minutes = 0or similar — fix it (manifest validation should catch this; file an issue if not).Verbose tracing flooding the file.
RUST_LOG=infoand retry.
Agent timeout fires every run
Profile the agent outside dotagent (time fish ./agent.fish). If it genuinely takes longer than timeout_seconds, bump the manifest. If it's fast in your shell but slow under the daemon — that's an environment difference (see env section above).
When all else fails
If you still can't pin it down, open an issue with the output of (1) through (4) and a redacted copy of the affected agent.toml.
Related
cli.md— every subcommand at a glancedaemon-lifecycle.md— start / stop / reloadobservability.md— log streams + jq recipespaths.md— where every file livesPlugin-specific troubleshooting under
docs/plugins/
Last updated
Was this helpful?