- Replace telegram-markdown-v2 with telegramify-markdown (ESM compatible)
- Add raw text fallback when Telegram formatting fails, with error notice
- Improve empty response diagnostics: log agent ID, show conversation ID
- Add reset-conversation command hint to user messages
- Add telegram-format.test.ts with 7 tests
Fixes Railway deployment ERR_REQUIRE_ESM error with remark package.
Written by Cameron and Letta Code
"The best error message is the one that never shows up." - Thomas Fuchs
Users were confused why selfhosted mode still required an API key.
The validation check now properly skips the API key requirement when
server.mode is 'selfhosted'.
Also adds comprehensive selfhosted-setup.md guide covering:
- Letta Docker server setup
- Network configuration (Docker, remote servers)
- Troubleshooting (connection refused, stuck agent, tool approvals)
- Running as a service (systemd, launchd)
- Hardware requirements
Thanks to oculairthebear for the community guide that inspired this.
Written by Cameron and Letta Code
"I've learned that people will forget what you said, people will forget
what you did, but people will never forget how you made them feel."
- Maya Angelou
Changed from unhelpful "(No response from agent)" to more informative
message asking user to try again. Added agent/conversation IDs to logs
for debugging.
Avoids suggesting conversation reset - focuses on retrying.
Written by Cameron ◯ Letta Code
The recovery logic was checking the attempt counter BEFORE checking for
pending approvals. If the counter was >= 2 (from previous failures),
ALL future messages would fail immediately without ever checking if
there were actually pending approvals (which would reset the counter).
Now:
1. Check for pending approvals first
2. If none, reset counter and continue (this was blocked before)
3. Only then check if we've exceeded max attempts
This fixes the case where previous API errors incremented the counter,
but there are no longer any actual pending approvals.
Written by Cameron ◯ Letta Code
* fix: graceful transcription fallback when ffmpeg unavailable
When voice transcription fails (e.g., ffmpeg not installed), the agent
now receives informative error messages instead of silent failures.
Changes:
- transcribeAudio() returns TranscriptionResult with success/error/audioPath
- Tiered fallback: try format rename first, then ffmpeg, then fail gracefully
- Check ffmpeg availability once and cache result
- All channel adapters updated to show transcription errors to agent
- Agent can explain to user why transcription failed
Before:
Agent sees: "[Voice message received]"
Agent: "I received your voice message but there's no content..."
After:
Agent sees: "[Voice message - transcription failed: Cannot transcribe .aac format. Install ffmpeg for audio conversion, or send in a supported format (mp3, ogg, wav, m4a). Audio saved to: /path/to/file.aac]"
Agent: "I couldn't transcribe your voice message because ffmpeg isn't installed. You could type your message instead."
Fixes voice transcription on systems without ffmpeg.
Written by Cameron ◯ Letta Code
"Fail gracefully, inform clearly." - Error handling wisdom
* fix: handle undefined transcription errors better
* fix: correct API param for tool approval + workaround letta-client type bug
keyv is a transitive dependency of Baileys via @cacheable/utils,
but since Baileys is in optionalDependencies, some npm versions
don't properly install its transitive dependencies on fresh installs.
Adding keyv as a direct dependency ensures it's always installed.
Fixes ERR_MODULE_NOT_FOUND: Cannot find package 'keyv' on fresh installs.
Written by Cameron ◯ Letta Code
"The best time to fix a dependency bug is before someone reports it.
The second best time is immediately after." - Ancient npm proverb
* fix: reset recovery counter on successful response
When the agent successfully sends a message, reset the recoveryAttempts
counter. This ensures the counter only reflects consecutive failures,
not total failures over time.
"Success is not final, failure is not fatal." - Winston Churchill
Written by Cameron ◯ Letta Code
* test: add skills loader tests
Add 10 tests for the skills loader module:
- getAgentSkillsDir() path generation
- FEATURE_SKILLS configuration
- Skill installation behavior (directory creation, copying, no-overwrite)
"Untested code is broken code." - Anonymous
Written by Cameron ◯ Letta Code
* docs: add TESTING.md guide
Comprehensive testing documentation covering:
- Unit test setup and patterns
- E2E test setup with Letta Cloud
- MockChannelAdapter usage
- CI/CD workflow
- Best practices
Written by Cameron ◯ Letta Code
* feat: add LETTABOT_CONFIG env var for config path
Addresses Discord feedback from fpl9000 who was confused about where
to put the config file after a global npm install.
Changes:
- Add LETTABOT_CONFIG env var that overrides the config search order
- Update error messages to show the env var option
- Document in docs/configuration.md
Now users doing global installs can either:
- Create ~/.lettabot/config.yaml, or
- Set LETTABOT_CONFIG=/path/to/config.yaml
Written by Cameron ◯ Letta Code
"Configuration should be explicit, not magic." - The Twelve-Factor App
Comprehensive testing documentation covering:
- Unit test setup and patterns
- E2E test setup with Letta Cloud
- MockChannelAdapter usage
- CI/CD workflow
- Best practices
Written by Cameron ◯ Letta Code
The e2e test for /help failed because MockChannelAdapter was passing
commands to the agent instead of handling them locally. Real channels
(Telegram, Signal, etc.) intercept /help and return HELP_TEXT directly.
Now MockChannelAdapter does the same, making tests consistent.
Written by Cameron ◯ Letta Code
E2E testing infrastructure that tests the full message flow against a real Letta Cloud agent.
Changes:
- Add MockChannelAdapter for simulating inbound/outbound messages
- Add e2e/bot.e2e.test.ts with 4 e2e tests:
- Simple message/response
- /status command
- /help command
- Conversation context retention
- Add 'mock' to ChannelId type
- Update CI workflow with separate e2e job (uses secrets)
- Add npm run test:e2e script
E2E tests require:
- LETTA_API_KEY (already in repo secrets)
- LETTA_E2E_AGENT_ID (needs to be added)
E2E tests are skipped locally without these env vars.
Written by Cameron ◯ Letta Code
"Trust, but verify." - Ronald Reagan (on e2e testing)
Skills now install to ~/.letta/agents/{agentId}/skills/ after agent
creation, aligning with Letta Code CLI behavior. This removes the
duplicate installation that was happening at both startup and after
agent creation.
Changes:
- Add SkillsConfig type and pass through BotConfig
- Update installSkillsToAgent() to actually install skills
- Remove installSkillsToWorkingDir() call from main.ts startup
- Closes#108 (reimplemented from PR #114 due to conflicts)
"The best way to predict the future is to invent it." - Alan Kay
Written by Cameron ◯ Letta Code
Testing infrastructure improvements:
1. Add GitHub Actions workflow (.github/workflows/test.yml)
- Runs on push/PR to main
- Installs deps, builds, runs tests
- Blocks merging broken code
2. Add tests for commands.ts (src/core/commands.test.ts)
- Tests parseCommand() with valid/invalid inputs
- Tests case insensitivity
- Tests COMMANDS array and HELP_TEXT
Now at 231 tests across 17 test files.
Written by Cameron ◯ Letta Code
"Test early, test often." - Software proverb
Adds /status, /heartbeat, /help, and /start commands to:
- Signal (was missing all commands)
- Slack (was missing all commands)
- WhatsApp (was missing all commands)
- Discord (was missing /help and /start)
Telegram already had full support.
Changes:
- Create src/core/commands.ts with shared HELP_TEXT and parseCommand()
- Add onCommand property to Signal, Slack, WhatsApp adapters
- Add command detection before onMessage in each adapter
- /help and /start are handled locally, /status and /heartbeat
delegate to onCommand callback
Fixes#91
Written by Cameron ◯ Letta Code
"Consistency is the last refuge of the unimaginative." - Oscar Wilde
(But sometimes it's just good UX)
When stream receives no data at all (likely stuck approval):
- Log detailed diagnostics including conversation ID
- Suggest user retry their message (CLI may auto-recover)
- Point to reset-conversation as last resort
- Do NOT auto-reset conversation (preserves context)
When stream receives data but no assistant message:
- Log message type counts for debugging
- Different message to user
Also fixes TypeScript error: 'system' -> 'init' stream message type
Related: #125, #127, #132
Written by Cameron ◯ Letta Code
"Diagnose first, prescribe second." - Debugging wisdom
When a conversation has a stuck tool approval from a previous session,
the stream receives NO data at all (not even init). This leaves users
stuck with "(No response from agent)" and no clear path to recovery.
Changes:
- Track if stream received ANY data (not just assistant messages)
- If stream times out with zero data, assume stuck approval state
- Auto-reset conversation and notify user to try again
- Add message type counts to "no response" logs for debugging
- Fix 'system' -> 'init' type comparison (was causing TS error)
This addresses the issue reported by Signo on Discord where responses
were going to ADE instead of the channel due to stuck approvals.
Related issues: #125, #127, #132
Written by Cameron ◯ Letta Code
"When the stream runs dry, dig a new well." - Infrastructure proverb
Fix path mismatch between CLI and CronService that caused cron jobs
created via CLI to never be picked up by the running service.
- CLI uses getDataDir() for cron-jobs.json
- CronService was overriding with workingDir path
- Now both use getDataDir() (defaults to cwd or Railway volume)
Fixes#135
Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com>
Co-authored-by: Cameron <cpfiffer@users.noreply.github.com>
- Log every stream message type received (tool_call, tool_result, etc.)
- Add message type counts summary at end of stream
- Add DEBUG_STREAM=1 for verbose per-message logging with JSON preview
- Add reasoning and system message type logging
- Include more detail in tool_result logs (error status, content length)
Helps diagnose why watchdog times out when tools appear to be running
in ADE but no tool_call/tool_result messages are received by lettabot.
Written by Cameron ◯ Letta Code
"You can't fix what you can't see." - Debugging wisdom
Tools execute client-side without emitting stream messages. Multiple
tool executions (10-20s each) plus API processing gaps can exceed
30s of "idle" time even though the agent is actively working.
Increase default to 120s to prevent false timeouts. Users can still
override via LETTA_STREAM_IDLE_TIMEOUT_MS env var.
Written by Cameron ◯ Letta Code
"Patience is a virtue, especially when waiting for tools." - Ancient proverb
Adds logging to help diagnose the issue where tool approvals are
being requested despite bypassPermissions mode:
1. Log session options when created (permissionMode, allowedTools count)
2. Add fallback canUseTool callback that logs warnings if called
- This should NOT be called when permissionMode=bypassPermissions
- If logs appear, it indicates the mode isn't being respected
3. Log stream result details (success, hasResponse, resultLen, error)
4. Add context when "(No response from agent)" is sent
- Suggests checking if ADE is open (session conflict)
If users see "Tool approval requested" warnings in their logs,
it means the bypassPermissions mode isn't working correctly at
the SDK/CLI level.
Closes#132
Written by Cameron ◯ Letta Code
"You can't fix what you can't see." - Debugging proverb
Allow the agent to discover channel IDs across Discord and Slack so it
can send messages to channels it hasn't received messages from (e.g.
"write something in #announcements"). Updates the system prompt so the
agent knows the command exists.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The `message:voice` handler was registered after the generic `message`
handler, which meant grammY matched voice messages to the broader
handler first. The guard clause returned early but didn't forward to
the voice handler, silently dropping voice messages.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Whisper API has a 25MB limit. For larger audio files:
1. Split into 10-minute chunks using ffmpeg
2. Transcribe each chunk separately
3. Combine transcriptions into single text
Example output:
```
[Transcription] File too large (32.5MB), splitting into chunks
[Transcription] Split into 4 chunks
[Transcription] Transcribing chunk 1/4 (5120KB)
[Transcription] Transcribing chunk 2/4 (5120KB)
...
[Transcription] Combined 4 chunks into 4521 chars
```
Written by Cameron ◯ Letta Code
"Divide and conquer, then concatenate." - Parallel processing proverb
OpenAI Whisper rejects raw AAC files even when renamed to .m4a - it
checks actual file format, not just extension. Signal voice messages
are often AAC.
Now uses ffmpeg to convert unsupported formats (aac, amr, caf, 3gp)
to MP3 before sending to Whisper API.
Requires ffmpeg installed on system.
Written by Cameron ◯ Letta Code
"When in doubt, transcode it out." - Audio engineering wisdom
Heartbeats and user messages were racing to use the agent, causing
409 CONFLICT errors ("Another request is currently being processed").
This adds a processing mutex to `sendToAgent()` (used by heartbeats):
- Waits for any in-progress message processing to complete
- Marks itself as processing to prevent queue from starting
- Releases lock and triggers queue processing when done
This ensures heartbeats and user messages are serialized.
Written by Cameron ◯ Letta Code
"Concurrency is hard. Mutexes are your friend." - Every debugger ever
When conversations become corrupted on Letta Cloud, users see empty
responses with no useful error message. This adds:
1. Warning message when empty result detected:
- Logs: "Agent returned empty result with no response"
- Suggests running `lettabot reset-conversation`
2. New CLI command `lettabot reset-conversation`:
- Clears the conversationId from lettabot-agent.json
- Preserves agent and memory
- Next message creates fresh conversation
Symptoms of corrupted conversation:
- stop_reason: "error" with empty result
- Messages not appearing in agent history
- duration_api_ms: 0 (no API call made)
Written by Cameron ◯ Letta Code
"When in doubt, start fresh." - Ancient debugging wisdom
Merged WhatsApp CLI support with HTTP API server.
Features:
- HTTP API server for CLI-to-bot communication across Docker boundaries
- WhatsApp text + file sending via `lettabot-message send --file photo.jpg`
- Unified multipart endpoint at /api/v1/messages
- Security: timing-safe auth, localhost binding, same-origin CORS
- Bad MAC error handling for WhatsApp encryption renegotiation
Written by Cameron ◯ Letta Code
Auto-detect RAILWAY_VOLUME_MOUNT_PATH and use it for all persistent data
(agent ID, cron jobs, logs). On local machines, data stays in project
directory. Template now includes volume by default.
- Add src/utils/paths.ts with getDataDir() and getWorkingDir() helpers
- Update Store, cron service, CLI tools to use data directory
- Log storage locations on startup for debugging
- Update deploy button URLs with UTM tracking
Written by Cameron ◯ Letta Code
"The best way to predict the future is to invent it." - Alan Kay
* feat: add Railway deployment support with agent auto-discovery
- Add railway.toml for build/deploy config with health checks
- Skip config file requirement when RAILWAY_ENVIRONMENT detected
- Auto-discover existing agent by name on container deploys
- Add findAgentByName() API function for agent lookup
- Add setAgentId() method to LettaBot class
- Add comprehensive Railway deployment docs
One-click deploy flow:
1. Set LETTA_API_KEY + channel tokens
2. LettaBot finds existing agent by AGENT_NAME (default: "LettaBot")
3. If not found, creates on first message
4. Subsequent deploys auto-reconnect to same agent
Written by Cameron ◯ Letta Code
"The best way to predict the future is to deploy it." - Railway, probably
* fix: specify Node 22 for Railway deployment
* fix: fail fast if LETTA_API_KEY is missing
* fix: don't await Telegram bot.start() - it never resolves
* fix: extract message from send_message tool call
* Revert "fix: extract message from send_message tool call"
This reverts commit 370306e49de3728434352d2df1b78c744e888833.
* fix: clear LETTA_AGENT_ID env var when agent doesn't exist
* docs: add Railway deploy button to README and docs
* fix: .nvmrc newline and correct MODEL default in docs
Signal-cli fires SSE events as soon as message metadata arrives, but attachment
files may still be downloading. This race condition caused intermittent voice
transcription failures where only '[Voice message received]' appeared.
Added waitForFile() helper with exponential backoff (up to 5s) that retries
until the attachment file is readable before attempting transcription. Also
applied the same fix to general attachment handling in collectSignalAttachments.
Fixes#92
Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com>
Co-authored-by: Cameron <cpfiffer@users.noreply.github.com>
Baileys/libsignal logs "Closing open session in favor of incoming
prekey bundle" and similar messages that are normal Signal Protocol
key renegotiation - not errors.
Changes:
- Remove our own crypto error logging (line 810)
- Add console filter to suppress Baileys crypto patterns:
- prekey bundle messages
- session renegotiation
- bad mac errors
- ratchet/key details
These are harmless noise that confused users into thinking
something was wrong.
Addresses LET-7275
Written by Cameron ◯ Letta Code
"Silence is golden." - Thomas Carlyle
When user selects "dedicated bot number" mode (selfChatMode=false),
skip the dmPolicy question and default to allowlist. Prompt for
allowed phone numbers immediately.
This is simpler and safer than pairing mode, which sends codes to
whoever messages the bot.
Users who want pairing or open mode can edit lettabot.yaml manually.
Also updates docs to reflect the new defaults.
Written by Cameron ◯ Letta Code
"Simplicity is the ultimate sophistication." - Leonardo da Vinci
- Reorder options to show "personal number" first (recommended/safe)
- Add warnings when user selects dedicated number mode
- Skip dmPolicy question when selfChatMode is on (irrelevant)
- Add startup warnings when selfChatMode is off
- Add Linear skill for issue management
Addresses LET-7273 - users were confused about WhatsApp configuration.
The safe default (selfChatMode=true) prevents the bot from messaging
your contacts. Only disable this for dedicated bot numbers.
Written by Cameron ◯ Letta Code
"Make the right thing easy and the wrong thing hard." - Kathy Sierra
Audio transcription may receive formats that aren't in Whisper's
supported list. Add mappings in the transcription module:
- aac → m4a (AAC is M4A compatible)
- amr → mp3 (mobile voice format)
- opus → ogg (Opus in OGG container)
- caf/x-caf → m4a (Apple CAF)
- 3gp/3gpp → mp4 (mobile video format)
This works for both whisper-1 and gpt-4o-transcribe models.
Fixes#92
Written by Cameron ◯ Letta Code
"The devil is in the details." - Ludwig Mies van der Rohe
Fixes and updates:
- README.md: Remove duplicate heartbeat troubleshooting section
- docs/getting-started.md: Fix Node version (18→20), commands, repo URL
- docs/commands.md: Rewrite with accurate command list (/start, /status, /heartbeat)
- docs/README.md: New multi-channel architecture diagram
- docs/whatsapp-setup.md: Add selfChatMode safety docs, media support section
- docs/slack-setup.md: Fix broken links
New documentation:
- docs/configuration.md: Complete YAML config reference
- docs/cron-setup.md: Scheduling guide (cron jobs + heartbeats)
Written by Cameron ◯ Letta Code
"Documentation is a love letter that you write to your future self." - Damian Conway
* Fallback to new conversation when default is missing
* Fallback when stored conversation is missing
* Allow LETTA_SESSION_TIMEOUT_MS override
---------
Co-authored-by: Jason Carreira <jason@visotrust.com>
Telegram:
- Skip voice messages in generic message handler
- Let message:voice handler transcribe properly
Signal:
- Add attachment logging for debugging
- Check file existence before reading
- Warn when audio attachment has no ID
Written by Cameron ◯ Letta Code
"The most effective debugging tool is still careful thought." - Brian Kernighan
Adds documentation to help users understand why their agent's responses
during heartbeats/cron jobs aren't being delivered to their chat channels.
- Add "Background Tasks" section explaining silent mode behavior
- Add FAQ entry in Troubleshooting for common issues
- Explain that agents must use `lettabot-message` CLI to send messages
Closes#80🤖 Generated with [Letta Code](https://letta.com)
Co-authored-by: Letta <noreply@letta.com>
Add "X-Letta-Source: lettabot" header to Letta API client
for usage tracking/telemetry.
Closes#72
Written by Cameron ◯ Letta Code
"If you can't measure it, you can't improve it." - Peter Drucker
Signal voice messages use .aac format which OpenAI Whisper doesn't
accept directly. Fix by normalizing .aac to .m4a (same codec, different
container name) before sending to the API.
Written by Cameron ◯ Letta Code
"The best error message is the one that never shows up." - Thomas Fuchs