Personal-only ambrogio-agent wrapper for Telegram with a secure /data boundary and Agent Skills-style skill loading.
- Telegram long polling input
- Automatic coalescing of consecutive Telegram messages before model execution (idle window)
- Telegram vocal message support with transcription (
gpt-4o-mini-transcribe) - Optional audio replies via ElevenLabs TTS with
/audio <prompt>whenELEVENLABS_API_KEYis set - Single-user allowlist (
TELEGRAM_ALLOWED_USER_ID) - Agent Skills-compatible discovery from
/data/.codex/skills/*/SKILL.md - Bootstrap automatico delle skill versionate in
./skillsverso/data/.codex/skills(missing + drift sync) - Docker hardening baseline (
read_only,cap_drop=ALL,no-new-privileges) - Dual backend support: OpenAI Codex (
codex exec) or Claude Code (claude -p) viaBACKENDenv var - Backend-tools-only mode via
codex execorclaude -p(no local fallback execution). - Minimal heartbeat loop every 30 minutes with
HEARTBEAT_OKsilent mode, explicitcheckin|alertactions, and Telegram delivery with dedup. - Soft-timeout for long requests (60s): user gets immediate "background job" feedback while Codex continues.
- Background job lifecycle persisted in SQLite with delivery retry.
- Three background job types:
- Immediate jobs (kind='background'): Long-running requests that timed out
- One-shot jobs (kind='delayed'): Future execution at a specific time (e.g., "fra 5 minuti...")
- Recurring jobs (kind='recurring'): Repeating scheduled execution (e.g., "ogni giorno alle 6")
- Natural-language job management (
list,inspect,retry,cancel,pause,resume) with explicit confirmation on ambiguity.
- Install deps:
bun install- Create env:
cp .env.example .env- Fill
.envvalues:
TELEGRAM_BOT_TOKENTELEGRAM_ALLOWED_USER_IDOPENAI_API_KEYBACKEND(default:codex, options:codexorclaude)CODEX_COMMAND(default:codex, only used whenBACKEND=codex)CODEX_ARGS(default:--dangerously-bypass-approvals-and-sandbox -c instructions=codex_fsinside this containerized setup)CLAUDE_COMMAND(default:claude, only used whenBACKEND=claude)CLAUDE_ARGS(optional additional args for Claude Code)HEARTBEAT_QUIET_HOURS(default suggested:22:00-06:00, local timezone; suppresses only timer check-ins)TELEGRAM_INPUT_IDLE_MS(default:3000, execute after this idle window)TELEGRAM_INPUT_BUFFER_ENABLED(default:true, disable to process each non-command update immediately)HOST_UID/HOST_GID(default:1000/1000, set to your host user id/group id when running containerized on macOS)MAC_TOOLS_ENABLED(default:false; keepfalsein containerized setup and run mac-tools on host)AMBROGIO_MAC_TOOLS_SOCKET_PATH(default:/data/runtime/mac-tools.sock)AMBROGIO_MAC_TOOLS_RPC_TRANSPORT(auto|unix|tcp, default:auto)AMBROGIO_MAC_TOOLS_TCP_HOST(container default:host.docker.internal)AMBROGIO_MAC_TOOLS_TCP_PORT(default:39223)AMBROGIO_MAC_TOOLS_TCP_ENABLED(host runner default:true)
- Start:
bun run devcp .env.example .env
mkdir -p data
docker compose up -d --buildThe codex CLI is installed via npm (@openai/codex), so ChatGPT login is available without building Codex from source.
BuildKit cache is enabled in compose and persisted in .docker-cache, so later rebuilds are significantly faster.
When using BACKEND=codex, use device auth to avoid localhost callback issues:
docker exec -it ambrogio-agent sh -lc 'HOME=/data CODEX_HOME=/data/.codex codex login --device-auth'
docker compose restart ambrogio-agentAuth data is persisted in the mounted ./data/.codex directory.
When using BACKEND=claude, authenticate via token:
docker compose exec ambrogio-agent sh -lc 'HOME=/data CLAUDE_HOME=/data/.claude claude setup-token'
docker compose restart ambrogio-agentThen add the token to your .env file:
CLAUDE_CODE_OAUTH_TOKEN=your-session-key-hereRequires a Claude subscription. Token is persisted in ./data/.claude.
All writable state is under ./data on the host, mounted to /data in the container.
The ambrogio-agent runs a dedicated heartbeat every 30 minutes (fixed interval, no configuration flags).
- Reads optional
/data/HEARTBEAT.mdinstructions. - Runs a lightweight model check with a runtime status block.
- Runtime status includes local timezone/date-time, heartbeat last state, idle duration, recent Telegram messages, conversation context (last 8 turns), TODO path, and TODO open-item snapshot (max 10).
- Runtime status includes job metrics: pending background deliveries, scheduled one-shot jobs, and active recurring jobs.
- If the model replies exactly
HEARTBEAT_OK, nothing is sent (unlessHEARTBEAT.mdexplicitly asks for an always-on notice message). - If action is needed, expected output is JSON:
{"action":"checkin|alert","issue":"...","impact":"...","nextStep":"...","todoItems":["..."]}
checkinandalertare different outbound messages.- Quiet hours can suppress timer-triggered
checkinmessages (alerts are never suppressed) viaHEARTBEAT_QUIET_HOURS. - Repeated timer-triggered heartbeat messages are deduplicated for 4 hours using persisted SQLite runtime keys.
- If heartbeat execution fails, it sends a Telegram alert.
- Alerts are sent to the most recent authorized chat seen at runtime.
/heartbeatforces an immediate run and returns a summary of the outcome./statusreports heartbeat interval/running/last run/last result plus idle and latest Telegram summary./clearresets conversation state, heartbeat runtime keys (including dedup keys), and task state.
Long operations automatically move to background after 60s timeout without killing Codex execution.
- Telegram immediately receives a message with
Job ID. - When the job finishes, the result is delivered automatically.
- If delivery fails, it is retried on the next heartbeat cycle.
- Immediate jobs (kind='background'): Chat requests that took too long
- One-shot jobs (kind='delayed'): Execute once at a specific future time
- Recurring jobs (kind='recurring'): Execute repeatedly on a schedule
One-shot and immediate jobs:
- "Mostra i task attivi"
- "Dammi i dettagli del task dl-..."
- "Ritenta il task dl-..."
- "Cancella il task precedente"
- "Tra 5 minuti mandami i top post di Hacker News"
Recurring jobs:
- "Dimmi se pioverà a Milano, ogni giorno alle 6 di mattina"
- "Check disk space every hour"
- "Mostra i job ricorrenti"
- "Metti in pausa il job rc-..."
- "Riprendi il job rc-..."
- "Cancella il job weather"
Temporarily mute jobs to prevent notifications until a specified time. Useful when you're already doing what the reminder was for (e.g., "I'm on the tram, stop alerting me").
Mute operations:
ambrogioctl jobs mute --id <jobId> --until <ISO timestamp>- Mute specific jobambrogioctl jobs mute-pattern --pattern <text> --until <ISO timestamp>- Mute jobs matching patternambrogioctl jobs unmute --id <jobId>- Unmute jobambrogioctl jobs list-muted [--limit N]- List currently muted jobs
Natural language:
- "I'm on the tram" → Mutes tram-related reminders until tomorrow morning
- "Stop bothering me about weather" → Mutes weather jobs
- "Show muted jobs" → Lists muted jobs
- "Unmute the tram reminders" → Clears mute on tram jobs
Behavior:
- One-shot jobs: Marked as
skipped_mutedwhen muted, never delivered - Recurring jobs: Continue scheduling but skip execution until unmuted
- Jobs automatically unmute when
muted_untiltime passes - All job deliveries include "⏰ [Background Job]" prefix
When runtime jobs and TODO intents are ambiguous, the ambrogio-agent asks explicit confirmation before executing.
Legacy commands (/tasks, /task <id>, /retrytask <id>, /canceltask <id>) remain available for debugging.
Skills can sync their SQLite state to human-readable markdown files for auditability.
Skills declare sync configuration in a SYNC.json manifest:
{
"version": "1",
"outputFile": "/data/MEMORY.md",
"patterns": ["memory:*"],
"generator": "./scripts/sync.sh",
"description": "Syncs semantic memory"
}The generator script formats data from SQLite to markdown:
#!/usr/bin/env bash
# Environment variables provided:
# - SYNC_OUTPUT_FILE: target file path
# - SYNC_PATTERNS: comma-separated patterns
# - SKILL_DIR: skill directory path
ambrogioctl state list --pattern "$SYNC_PATTERNS" --json | \
# ... format as markdown ...
> "$SYNC_OUTPUT_FILE"# List skills with sync capability
ambrogioctl sync list
# Generate sync file for specific skill
ambrogioctl sync generate --skill memory-manager
# Generate for all skills
ambrogioctl sync generate --all
# Validate manifest
ambrogioctl sync validate --skill memory-manager- memory-manager: Syncs to
/data/MEMORY.md- semantic memory with preferences, facts, and patterns - structured-notes: Syncs to
/data/NOTES.md- organized notes by type (project, decision, log) with tags
Ambrogio exposes a local Unix-socket job RPC server:
- Socket path:
/tmp/ambrogio-agent.sock(override withAMBROGIO_SOCKET_PATH) - Protocol: one-line JSON request/response envelopes (
ok/resultorok=false/error)
CLI client:
One-shot and immediate jobs (tasks scope):
bun run ctl -- tasks list --json
bun run ctl -- tasks inspect --id <task-id> --json
bun run ctl -- tasks create --run-at 2099-01-01T10:00:00.000Z --prompt "..." --user-id 123 --chat-id 123 --json
bun run ctl -- tasks cancel --id <task-id> --json
bun run ctl -- tasks retry --id <task-id> --jsonRecurring jobs (jobs scope):
bun run ctl -- jobs create-recurring --run-at 2099-01-01T10:00:00.000Z --prompt "..." --user-id 123 --chat-id 123 --type interval --expression "1h" --json
bun run ctl -- jobs list-recurring --json
bun run ctl -- jobs pause --id <job-id> --json
bun run ctl -- jobs resume --id <job-id> --json
bun run ctl -- jobs update-recurrence --id <job-id> --expression "2h" --jsonTelegram media:
bun run ctl -- telegram send-photo --path /data/path/to/image.png --json
bun run ctl -- telegram send-audio --path /data/path/to/audio.mp3 --json
bun run ctl -- telegram send-document --path /data/path/to/file.pdf --jsonmacOS native tools (when MAC_TOOLS_ENABLED=true):
bun run ctl -- mac ping --json
bun run ctl -- mac info --json
bun run ctl -- mac calendar upcoming --days 7 --limit 100 --timezone Europe/Rome --json
bun run ctl -- mac reminders open --limit 200 --include-no-due-date true --jsonTCC troubleshooting:
- Open
System Settings > Privacy & Security > CalendarsandReminders - Allow access for the process running Ambrogio (or your terminal)
- Retry the
ambrogioctl mac ...command
When the agent runs in Docker, keep the macOS tools service on the host:
- Configure
.env:
HOST_UID=$(id -u)
HOST_GID=$(id -g)
MAC_TOOLS_ENABLED=false
AMBROGIO_MAC_TOOLS_SOCKET_PATH=/data/runtime/mac-tools.sock
AMBROGIO_MAC_TOOLS_RPC_TRANSPORT=auto
AMBROGIO_MAC_TOOLS_TCP_HOST=host.docker.internal
AMBROGIO_MAC_TOOLS_TCP_PORT=39223
AMBROGIO_MAC_TOOLS_TCP_ENABLED=true- Start mac-tools service on host (outside Docker):
mkdir -p data/runtime
bun run mac-tools:hostThe host runner exposes both Unix socket and TCP (default 0.0.0.0:39223) for Docker fallback.
- Start/restart container:
docker compose up -d --build- Test from the containerized CLI:
bun run ctl -- mac ping
bun run ctl -- mac infoExample HEARTBEAT.md:
# Heartbeat
- Use Runtime status as source of truth.
- Review idle duration, recent messages, and TODO snapshot.
- If no action is needed, reply exactly HEARTBEAT_OK.
- If action is needed, reply with JSON only:
{"action":"checkin|alert","issue":"...","impact":"...","nextStep":"...","todoItems":["..."]}Le skill che vuoi portare tra host vanno versionate nel repository:
skills/<skill-id>/SKILL.md
All'avvio, il processo sincronizza automaticamente le skill da ./skills a /data/.codex/skills:
- copia le skill mancanti;
- aggiorna le skill gia presenti quando
SKILL.mddiverge dalla versione nel repository; - lascia inalterate le skill gia allineate.
Se serve, puoi cambiare sorgente con PROJECT_SKILLS_ROOT.
The service runs either codex exec or claude -p per request based on BACKEND env var.
Codex mode:
--output-last-messagecaptures the final assistant message to a file- Tool execution handled inside Codex runtime
Claude mode:
--output-format jsonreturns structured response via stdout--no-session-persistencesince conversation state managed externally- Tool execution handled inside Claude runtime
File/photo/audio delivery is performed through local RPC (ambrogioctl telegram ...), not XML-like output tags.
bun test
bun run typecheckInstall from host into the mounted skills directory:
python3 /Users/daniele/.codex/skills/.system/skill-installer/scripts/install-skill-from-github.py \
--repo elevenlabs/skills \
--path text-to-speech \
--dest /Users/daniele/Code/ambrogio-agent/data/.codex/skills