- Dashed underline: a design decision of mine, drafted together.
- Margin note: an argument or finding the model contributed, not me.
Status: working · MIT license · macOS only Repository: github.com/tomasgauthier/latido
An agent that wakes up on its own, checks whether anything worth knowing has happened, and messages you on Telegram when it has. On the vast majority of beats it says nothing — which is the point: one that reports “all good” every few hours gets muted within two days.
1. The gap it fills
The coding agents I use every day share a limitation you don’t notice until you go looking for it: they all wait to be called. The session is the unit. You type, it answers, you close the terminal, and it stops existing.
That’s enough to get work done. It isn’t enough for the other thing: for something to tell you what you didn’t ask about, when you aren’t looking. A Monday at eight in the morning, laptop shut, because a proposal has been sitting untouched in the inbox for nine days.
Mechanisms that look like this exist, and none of them serve. Session crons live in memory and die with it. Scheduled cloud agents can’t see your disk. And session remote controls — which connect you from your phone to something already running — solve the opposite direction: they bring you closer to your session, but they will never write to you.
All three share the same defect: they depend on something staying alive. And an open session is precisely what you won’t have on a Saturday morning.
2. The idea
That it depend on no particular session. That was the requirement, and everything else falls out of it.
launchd wakes an agent CLI in its non-interactive mode. Every beat is a fresh session that lives thirty seconds and dies. Continuity isn’t in the session: it’s in a text file the agent reads on waking and rewrites before going back to sleep.
You can close everything and it keeps beating. The context you’d think gets lost isn’t lost, because the sources are durable and live on disk — the notes vault, the inbox, the journal. A cloud agent can read none of that. This one can.
3. The rule that makes it bearable
If there’s nothing to say, it says nothing.
That’s the first line of its instructions, and it isn’t an aesthetic preference. A heartbeat reporting “all good” every four hours gets muted within two days, and that’s the end of the project: the one failure that kills a proactive assistant is becoming noise.
To speak, all three conditions must hold at once:
- It’s something you’d want to know now, not next time you sit down.
- You don’t already know it — it isn’t in its memory as already reported.
- There’s something to do about it. A fact that changes no decision isn’t news, it’s noise.
Plus an exclusion rule that avoids the obvious duplication: nothing already visible at a glance in the menu bar. Services down, memory, backups, usage percentages. Monitors exist for that. Its job is what takes judgment, not what takes a number.
4. Listening is free, thinking is what costs
The first version didn’t listen. The agent fetched messages when it woke, so the latency of a reply was the whole cadence: at four hours, a question could sit for four hours.
The way out was separating two things we’d been treating as one. A long-polling process consumes nothing: it’s an open socket waiting. What costs is waking the model.
Three launchd agents came out of it:
| Agent | What it is | How it stays up |
|---|---|---|
| the ear | Waits for a message and fires a beat instantly | KeepAlive — revived if it dies |
| the clock | Looks on its own every N hours | StartInterval |
| the page | Configuration on localhost | KeepAlive |
With that it answers you right away when you write, and keeps its own round for telling you things unprompted. The ear isn’t a live agent session waiting: it’s a thirty-line while True.
The ear is also the only thing that reads from Telegram. Two consumers of the same queue steal each other’s messages — a mistake we made and that took a while to understand — so it drops what arrives into a file and the beat reads it from there.
5. So it doesn’t die quietly
Here’s a trap the design sets for itself.
If silence is its normal result, a broken heartbeat looks exactly like a quiet one. If the CLI’s session expires tomorrow, or a path changes, or the binary moves, the agent fails silently and you notice nothing — because noticing nothing is exactly what you expect from it. You might find out three weeks later.
That’s why it writes a marker file when it finishes, but only if the beat went well. If it measured execution instead of success, one that runs and fails every time would look healthy. Any freshness monitor pointed at that file — in my case the menu bar I already had — raises the alarm when the pulse stops.
It leaves a clean division between the two: the monitor watches the heartbeat, the heartbeat watches your life. Each does what the other is forbidden to do.
6. Any CLI
The tool was not to be tied to one provider. The invocation lives in configuration, not in code:
"cli": {
"bin": "claude",
"args": ["-p", "{prompt}", "--model", "{modelo}",
"--permission-mode", "acceptEdits",
"--allowedTools", "{herramientas}"],
"flag_carpeta": "--add-dir"
}
Any CLI that takes a prompt as an argument and returns text on stdout will do. The page detects which ones you have installed and ships presets for the known ones, with flags read from each one’s help output. Where it wasn’t tested thoroughly, the card says so: a preset that promises to work and doesn’t is worse than none.
The only non-negotiable on the other side is that the model can write a file. That file is its one and only voice: if it decides to speak, it writes there, and the runner sends it.
On terms of service
It’s a fair question when something runs unattended with your session, and it deserves an answer you can actually stand behind.
The latido asks nothing special of anyone: it runs your provider’s official CLI, in its non-interactive mode, with your own already-authenticated session. It does not extract credentials, forward them, stand up a proxy, drive a user interface, or scrape anything. It is exactly what you would type in your terminal — only a timer types it.
What the repository deliberately does not claim is that it complies with every provider’s terms: each has its own and they change. Promising that in a README is standing behind something you cannot verify. Reading them is on whoever runs it.
7. The record
What the agent writes about your life doesn’t go in the repository: it goes where you read. In my case, the Obsidian vault, like any other note.
Two files. A log, one file per day and one line per beat — what it said, or why it stayed quiet — and its working memory between one beat and the next. That log is what makes the thing tunable: you read why it stayed quiet and adjust the instructions until it speaks when it should.
Keeping them out of the repository isn’t only convenience. The memory enters the context of every beat in full, and the log is a diary: publishing the code together with the record of someone’s Wednesdays is an accident waiting to happen.
8. What it isn’t
It isn’t a conversational assistant. It’s one short shot, with a counted list of tools and a strong bias toward silence.
To ask something with a session already open, a remote control beats it at everything: full power, real conversation, any repository.
| A remote control | A heartbeat | |
|---|---|---|
| Who starts | You, always | It does too |
| Needs a live session | Yes | No: it creates one |
| Reach | Full, conversational | One short shot, few tools |
| When nothing is open | Nothing to connect to | Still works |
The whole difference fits in the first row. A remote control has no initiative. It’s a narrow moat, but it’s the one a remote control cannot cross.
9. Install
git clone https://github.com/tomasgauthier/latido && cd latido
cp config.example.json config.json && chmod 600 config.json
./servidor.py --instalar
open http://127.0.0.1:8737
Everything else is configured from that page: the Telegram bot, the folders it watches — with a browser, no typing paths — the engine, the pace and the instructions. That anyone could clone it and set it up without touching a file was part of the brief, so no paths are hardcoded anywhere: the launchd agents are generated with the location of the repository you just cloned.
The server listens on 127.0.0.1 only. It edits files and loads system agents; it has no business on the network.
The agent’s instructions ship in Spanish, so out of the box it writes to you in Spanish. An English version is included in the repository — one copy command and you’re done. That file is plain text and it is the whole personality.
Sources and references
The repository
- github.com/tomasgauthier/latido — code, README in Spanish and English, and the agent’s instructions, which are an editable text file.
The operating system pieces
launchdand user agents — Apple. WhereStartIntervalandKeepAlivecome from, the two forms of life the clock and the ear use.
Telegram
- Bot API —
getUpdates— Telegram. Long polling, the offset that confirms read messages, and the 24 hours it holds pending ones: that’s what makes a permanently listening process unnecessary.
The authorship convention on this page
- weid — the convention this text is marked with.