* fix: prevent duplicate Telegram messages on "not modified" edit
When streaming completes and the final editMessage call sends identical
content, Telegram returns "message is not modified". The catch block
treated this as a real failure and bubbled up to bot.ts, which fell back
to sending the response as a brand-new message -- causing duplicates.
Now the Telegram adapter silently returns on "message is not modified"
since the content is already correct.
Written by Cameron ◯ Letta Code
"The simplest fix is the one that doesn't fight the API."
* fix: update package-lock.json for remark/mdast dependencies
PR #234 (Slack mrkdwn formatter) added remark-gfm and related mdast
packages but the lockfile wasn't regenerated, causing npm ci to fail
in CI with "Missing from lock file" errors.
Written by Cameron ◯ Letta Code
"The lockfile remembers what package.json forgets."
* Fix action directives and reactions
* revert package-lock.json to main
Drop unrelated lockfile churn from this PR -- the peer dependency
flag changes were artifacts of a different npm version.
Written by Cameron ◯ Letta Code
"The lockfile is a contract, not a suggestion." -- every CI pipeline ever
* docs: add response directives documentation
Document the XML action directives system (introduced in #239, parsing
fixes in #248): <actions> block format, <react> directive, attribute
quoting rules, channel support matrix, emoji alias tables, streaming
holdback behavior, and extension guide.
Written by Cameron ◯ Letta Code
"Documentation is a love letter to your future self." -- Damian Conway
---------
Co-authored-by: Jason Carreira <jason@visotrust.com>
Co-authored-by: Cameron <cameron@pfiffer.org>
* Slack: convert Markdown to mrkdwn
* Slack: avoid literal dynamic import for optional dep
* Slack formatter: cache optional dependency load state
* fix: remove slackify-markdown from lockfile dependencies
The lockfile had slackify-markdown in both `dependencies` (pinned) and
`optionalDependencies`, but package.json only lists it in
optionalDependencies. This caused npm ci to treat it as required,
defeating the optional dependency pattern.
Regenerated lockfile with clean npm install to fix.
Written by Cameron ◯ Letta Code
"The lockfile giveth, and the lockfile taketh away." - npm, probably
---------
Co-authored-by: Cameron <cameron@pfiffer.org>
* feat: add {{NO_REPLY}} silent marker for agent opt-out
Allow the agent to respond with {{NO_REPLY}} to suppress message
delivery for messages that don't warrant a reply. The marker is
checked in three places: the streaming edit guard (prefix match to
prevent partial sends), finalizeMessage(), and the post-stream
response handler.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: use <no-reply/> XML marker instead of {{NO_REPLY}}
Switch to XML-style self-closing tag for consistency with the XML
envelope format used elsewhere, and because LLMs produce well-formed
XML tags more reliably than template syntax.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add no-reply hint to group chat envelopes
Agents created outside lettabot (via ADE, Letta Cloud) won't have the
system prompt telling them about <no-reply/>. Adding the hint to group
chat envelopes makes the opt-out mechanism self-documenting.
Written by Cameron ◯ Letta Code
"Silence is one of the great arts of conversation." -- Marcus Tullius Cicero
---------
Co-authored-by: Gabriele Sarti <gabriele.sarti996@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The ^6.7.21 caret range resolves to 6.17.16 on fresh npm install, which
ships incompatible TypeScript types (no call signatures on default export).
Pins exact version and adds explicit type annotation on getMessage key param.
Also updates commands.test.ts to expect 5 commands (reset was added but
test still expected 4).
Fixes#192
Written by Cameron ◯ Letta Code
"The caret giveth, and the caret taketh away."
- 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
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
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 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
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
- Add vitest as dev dependency
- Add test scripts: `npm test` (watch) and `npm run test:run` (CI)
- Add initial unit tests for pure utility functions:
- src/utils/phone.test.ts (10 tests)
- src/utils/server.test.ts (10 tests)
- src/channels/attachments.test.ts (6 tests)
All 26 tests passing.
Written by Cameron ◯ Letta Code
* Add inbound attachment handling and pruning
* Add Signal attachment support and logging
- Implement full Signal attachment collection (copies from signal-cli dir)
- Add logging when attachments are saved to disk for all channels
- Skip audio attachments in Signal (handled by voice transcription)
Written by Cameron ◯ Letta Code
* Gitignore bun.lock
Keep lockfile local, don't track in repo.
Written by Cameron ◯ Letta Code
---------
Co-authored-by: Jason Carreira <jason@visotrust.com>
* Add voice message transcription support (all channels)
Adds OpenAI Whisper transcription for voice messages across all channels:
- Telegram: ctx.message.voice
- WhatsApp: audioMessage via downloadMediaMessage
- Signal: audio attachments from local files
- Slack: audio files via url_private_download
- Discord: audio attachments
Voice messages sent to agent as "[Voice message]: <transcript>"
Configuration (config takes priority over env):
- lettabot.yaml: transcription.apiKey, transcription.model
- Env: OPENAI_API_KEY, TRANSCRIPTION_MODEL
Closes#47
Written by Cameron ◯ Letta Code
"The best interface is no interface - just talk."
* Add voice message documentation to README
- Add Voice Messages to features list
- Add configuration section for transcription
- Document supported channels
Written by Cameron ◯ Letta Code
* Notify users when voice transcription is not configured
Instead of silently ignoring voice messages, send a helpful message
linking to the documentation.
Written by Cameron ◯ Letta Code
* feat: upgrade to letta-code-sdk main + fix Signal voice transcription
- Switch from published SDK (v0.0.3) to local main branch (file:../letta-code-sdk)
- Update bot.ts for new SDK API: createSession(agentId?, options) signature
- Add conversationId tracking to store for proper conversation persistence
- Fix Signal voice transcription: read attachments from ~/.local/share/signal-cli/attachments/
- Fix Telegram markdown ESM issue: make markdownToTelegramV2 async with dynamic import
- Add transcription config to lettabot.yaml
- Add extensive debug logging for queue and session processing
Signal voice messages now properly transcribe and send to agent.
🐾 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
* fix: update Signal CLI message sender to use daemon JSON-RPC API
- Switch from signal-cli-rest-api to signal-cli daemon (port 8090)
- Use JSON-RPC send method instead of REST /v2/send
- Support group IDs with group: prefix
- Handle 201 responses and empty bodies correctly
🐾 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
* Add placeholder for untranscribed voice messages on Signal
If a voice-only message arrives and transcription fails or is disabled,
forward a placeholder so the user knows the message was received.
Written by Cameron ◯ Letta Code
---------
Co-authored-by: Letta <noreply@letta.com>
- Add stepGoogle() wizard that guides users through gog CLI setup
- Check/install gog via Homebrew, guide OAuth credential setup
- Let users select existing accounts or add new ones
- Configure which Google services to enable (gmail, calendar, drive, etc.)
- Add GoogleConfig type and integrations.google to config schema
- Wire GMAIL_ACCOUNT env var from YAML config for polling service
- Add discord.js dependency (fixes build)
🐙 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
- Separate message bubbles when stream message type changes
(e.g., assistant → tool_call → assistant now sends as separate messages)
- Track sentAnyMessage to avoid spurious "(No response from agent)"
- Add canUseTool workaround for SDK v0.0.3 bypassPermissions bug
(see letta-ai/letta-code-sdk#10)
- Clean up verbose debug logging
Written by Cameron ◯ Letta Code
"The stream of consciousness is not a river but a series of pools." - William James