445 Commits

Author SHA1 Message Date
Cameron
abf3307e3d fix: include allowedUsers in env var channel fallback (#226)
normalizeAgents() env var fallback (added in #224) only mapped token
and dmPolicy, missing allowedUsers for all channels. This caused
dmPolicy=allowlist to reject everyone since the allowlist was empty.

Parses *_ALLOWED_USERS env vars (comma-separated) for all five
channels, matching the existing format used by onboard.ts and the
Railway env exporter.

Written by Cameron ◯ Letta Code

"We can only see a short distance ahead, but we can see plenty there that needs to be done." -- Alan Turing
2026-02-08 23:07:32 -08:00
Cameron
65cd82bc33 fix: restore env var channel config for container deploys (#224)
normalizeAgents() only read YAML config, breaking Railway/Docker
deploys where channels are configured via environment variables
(TELEGRAM_BOT_TOKEN, etc.). Add env var fallback in the legacy
single-agent path.

Multi-agent mode still requires YAML configuration.

Written by Cameron ◯ Letta Code

"The cheapest, fastest, and most reliable components are those that aren't there." -- Gordon Bell
2026-02-08 22:42:22 -08:00
Cameron
61a7450106 docs: multi-agent configuration reference (#223)
* docs: add multi-agent configuration reference

Document the agents[] YAML config, per-agent options, migration path
from single to multi-agent, and known limitations (#219, #220, #221).

Written by Cameron ◯ Letta Code

"Documentation is a love letter that you write to your future self." -- Damian Conway

* docs: fix channels required claim and soften isolation wording

- channels is not strictly required per-agent (validation is global)
- isolation has known exceptions, don't claim "fully isolated"

Written by Cameron ◯ Letta Code

"Clear is kind." -- Brene Brown
2026-02-08 22:36:25 -08:00
Cameron
5200a1e7e8 fix: false "no response" error after short streaming replies (#222)
When the agent sends a short single-chunk response (e.g. "Yep."), the
streaming edit path sends it to the channel immediately but never sets
sentAnyMessage. When finalizeMessage() then tries to edit the message
to identical content, Telegram rejects it ("message not modified"),
the catch swallows the error, and the post-loop fallback shows a
spurious "(No response)" error alongside the actual reply.

Fix: (1) set sentAnyMessage when the streaming path sends a new message,
(2) treat edit failures as success when messageId exists (the message
is already displayed to the user).

Written by Cameron ◯ Letta Code

"The stream carried everything -- we just forgot to look." - on debugging
2026-02-08 22:26:25 -08:00
Cameron
40586cdc9a fix: multi-agent state isolation and config.id wiring (#218)
Two bugs from the Phase 1 merge:

1. Store not scoped to agent name: LettaBot constructor passed no agent
   name to Store, so all agents in multi-agent mode shared the same
   agentId/conversationId state. Now passes config.agentName.

2. agentConfig.id ignored: Users could set `id: agent-abc123` in YAML
   but it was never applied. Now checked before store verification.

Written by Cameron ◯ Letta Code

"The best error message is the one that never shows up." -- Thomas Fuchs
2026-02-08 21:56:47 -08:00
Cameron
2f5242decd feat: wire up multi-agent in main.ts (Phase 1c) (#217)
Multi-agent configs now work end-to-end. Users can write `agents:` in
lettabot.yaml and each agent gets its own channels, cron, heartbeat,
and polling services.

- Rewrite main() to loop over normalizeAgents() output
- Extract createChannelsForAgent() and createGroupBatcher() helpers
- Replace env-var config parsing with YAML-direct for per-agent config
- LettaGateway manages all agents, API server uses gateway for delivery
- Per-agent services: cron, heartbeat, polling are agent-scoped
- Store v2 format handled at startup for LETTA_AGENT_ID loading
- Legacy single-agent configs work unchanged via normalizeAgents()

Part of #109

Written by Cameron ◯ Letta Code

"Make it work, make it right, make it fast." -- Kent Beck
2026-02-08 21:42:42 -08:00
Cameron
2fbd767c50 feat: add AgentSession interface and LettaGateway orchestrator (Phase 1b) (#216)
Interface-first multi-agent orchestration layer.

- Define AgentSession interface capturing the contract consumers depend on
- LettaBot implements AgentSession (already has all methods, now explicit)
- LettaGateway manages multiple named AgentSession instances
- Update heartbeat, cron, polling, API server to depend on interface, not concrete class
- 8 new gateway tests

No behavioral changes. Consumers that used LettaBot now use AgentSession interface,
enabling multi-agent without modifying consumer code.

Part of #109

Written by Cameron ◯ Letta Code

"First, solve the problem. Then, write the code." -- John Johnson
2026-02-08 21:41:45 -08:00
Cameron
3339a880f1 feat: multi-agent config types, normalizeAgents, and Store v2 (Phase 1a) (#215)
* feat: add multi-agent config types, normalizeAgents, and Store v2

Foundation for multi-agent support (Phase 1a). No behavioral changes.

- Add AgentConfig interface for per-agent configuration
- Add agents[] field to LettaBotConfig for docker-compose style multi-agent configs
- Add normalizeAgents() to convert legacy single-agent config to agents[] array
- Evolve Store to v2 format with per-agent state isolation
- Auto-migrate v1 store files to v2 transparently
- 18 new tests for normalization and store migration

Part of #109

Written by Cameron ◯ Letta Code

"The only way to do great work is to love what you do." -- Steve Jobs

* fix: harden multi-agent normalization and store isolation

* style: simplify redundant WhatsApp enabled check

WhatsApp has no credential to check (uses QR pairing), so the
`enabled !== false && enabled` condition simplifies to just `enabled`.

Written by Cameron ◯ Letta Code

"Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away." -- Antoine de Saint-Exupery
2026-02-08 21:39:48 -08:00
Gabriele Sarti
110681e979 feat: pass images to the LLM via multimodal API (#184)
feat: pass images to the LLM via multimodal API

When users send images through any channel, the actual image content is now passed to the LLM via the SDK's multimodal API (imageFromFile/imageFromURL) instead of just text metadata.

- Graceful fallback for unsupported MIME types, missing files, and load errors
- Opt-out via features.inlineImages: false in config
- Warns when model doesn't support vision (detects [Image omitted] in response)
2026-02-08 20:22:32 -08:00
Cameron
528ef1f40e fix: add deprecation warning for agent.model + e2e model tests
- Warn at startup when agent.model is present in lettabot.yaml (deprecated, now ignored)
- Add e2e tests for model listing and agent model retrieval
- Remove stale model field from existing e2e test (BotConfig no longer has it)

Written by Cameron ◯ Letta Code

"The best way to predict the future is to invent it." -- Alan Kay
2026-02-08 20:09:39 -08:00
Cameron
64f12be6cd docs: add api.port config reference + Telegram message splitting note (#206)
- Document api.port/host/corsOrigin in configuration.md (example,
  reference table, and env var mapping)
- Add "Long Messages" section to telegram-setup.md noting the
  automatic 4096-char splitting behavior

Written by Cameron ◯ Letta Code

"The best time to plant a tree was 20 years ago. The second best time is now." - Chinese Proverb
2026-02-08 16:48:32 -08:00
Cameron
56e3df17d2 feat: persist voice message audio files to disk (#207)
Voice messages are now saved to the attachments directory regardless of
transcription outcome. The audio file path is included in the message
envelope so agents always have access to the original audio, even when
transcription fails or returns empty.

🐾 Generated with [Letta Code](https://letta.com)

Co-authored-by: Letta <noreply@letta.com>
2026-02-08 16:48:21 -08:00
Gabriele Sarti
66e8c462bf feat: group message batching + Telegram group gating + instantGroups (#187)
* feat: add group message batching, Telegram group gating, and instantGroups

Group Message Batcher:
- New GroupBatcher buffers group chat messages and flushes on timer or @mention
- Channel-agnostic: works with any ChannelAdapter
- Configurable per-channel via groupPollIntervalMin (default: 10min, 0 = immediate)
- formatGroupBatchEnvelope formats batched messages as chat logs for the agent
- Single-message batches unwrapped to use DM-style formatMessageEnvelope

Telegram Group Gating:
- my_chat_member handler: bot leaves groups when added by unpaired users
- Groups added by paired users are auto-approved via group-store
- Group messages bypass DM pairing (middleware skips group/supergroup chats)
- Mention detection for @bot in group messages

Channel Group Support:
- All adapters: getDmPolicy() interface method
- Discord: serverId (guildId), wasMentioned, pairing bypass for guilds
- Signal: group messages bypass pairing
- Slack: wasMentioned field on messages

instantGroups Config:
- Per-channel instantGroups config to bypass batching for specific groups
- For Discord, checked against both serverId and chatId
- YAML config → env vars → parsed in main.ts → Set passed to bot

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: preserve large numeric IDs in instantGroups YAML config

Discord snowflake IDs exceed Number.MAX_SAFE_INTEGER, so YAML parses
unquoted IDs as lossy JavaScript numbers. Use the document AST to
extract the original string representation and avoid precision loss.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Slack dmPolicy, Telegram group gating check

- Add dmPolicy to SlackConfig and wire through config/env/adapter
  (was hardcoded to 'open', now reads from config like other adapters)
- Check isGroupApproved() in Telegram middleware before processing
  group messages (approveGroup was called but never checked)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 14:47:22 -08:00
Cameron
c88621574a fix: remove StreamWatchdog that kills long-running agent operations (#204)
The watchdog aborted streams after idle timeouts, which breaks legitimate
subagent operations. The SDK stream should throw on connection failures.

Written by Cameron ◯ Letta Code

"The stream will end when it's ready." - a patient engineer
2026-02-07 14:43:01 -08:00
github-actions[bot]
2fe5ebe06d feat: expose polling configuration in lettabot.yaml (#202)
Add a top-level `polling` section to lettabot.yaml for configuring
background polling (Gmail, etc.) instead of relying solely on env vars.

- Add `PollingYamlConfig` type with `enabled`, `intervalMs`, and `gmail` subsection
- Update `configToEnv()` to map new polling config to env vars
- Update `main.ts` to read from YAML config with env var fallback
- Maintain backward compat with `integrations.google` legacy path
- Document polling config in docs/configuration.md

Fixes #201

Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com>
Co-authored-by: Cameron <cpfiffer@users.noreply.github.com>
2026-02-07 13:09:05 -08:00
Cameron
9a1fd68b7e fix: add api.port config + Telegram message splitting + error handling hardening (#200)
Addresses two community-reported issues:

1. Port configuration: The API server port was only readable from the
   PORT env var. Add api.port/host/corsOrigin to lettabot.yaml schema
   so users can configure it alongside other settings.

2. Telegram sending failures: Messages exceeding Telegram's 4096 char
   limit would fail with no splitting. Add splitMessageText() that
   splits at paragraph/line boundaries (~3800 chars to leave room for
   MarkdownV2 escaping), with a safety net re-split if formatting
   expands beyond 4096. Also wrap two unguarded adapter.sendMessage()
   calls in bot.ts error paths that could cascade into unhandled
   rejections crashing the process.

Written by Cameron ◯ Letta Code

"When you can't make them see the light, make them feel the heat." - Ronald Reagan
2026-02-07 11:20:07 -08:00
Cameron
9cb35228fd fix: deduplicate tool_call stream events by toolCallId (#199)
The Letta server streams tool_call_message events token-by-token as
the model generates tool arguments. A single tool call (e.g.
memory_rethink with a large new_memory arg) can produce hundreds of
wire events, all sharing the same toolCallId. Without dedup, the bot
miscounts these as separate tool calls -- logging "Calling tool: X"
hundreds of times and potentially triggering the tool-loop abort at
maxToolCalls (default 100) for what is actually a single call.

Track seen toolCallIds per stream and skip chunks already yielded.

Written by Cameron and Letta Code

"In the beginner's mind there are many possibilities, but in the
expert's mind there are few." -- Shunryu Suzuki
2026-02-06 16:52:33 -08:00
letta-code
7e82374865 fix: remove model field from lettabot config, add lettabot model command
The model field in lettabot.yaml was redundant and misleading -- the model
is configured on the Letta agent server-side, and lettabot shouldn't
override it. Users were confused seeing a model in their startup log that
didn't match the actual model being used.

Changes:
- Remove `model` from `LettaBotConfig.agent` (made optional for backward compat)
- Remove `model` from `BotConfig` interface and `bot.ts` createAgent() calls
- Remove `model` from `main.ts` config construction and startup log
- Stop saving `model` to lettabot.yaml during onboarding
- Stop mapping `agent.model` to `MODEL` env var in config/io.ts
- Add `getAgentModel()` and `updateAgentModel()` to letta-api.ts
- Add new `src/commands/model.ts` with three subcommands:
  - `lettabot model` - interactive model selector
  - `lettabot model show` - show current agent model
  - `lettabot model set <handle>` - set model directly
- Wire up model command in cli.ts with help text
- Update docs/configuration.md, lettabot.example.yaml, SKILL.md

Model selection during `lettabot onboard` is preserved for new agent
creation -- the selected model is passed to createAgent() but is NOT
saved to the config file.

Fixes #169

Co-authored-by: Cameron <cpfiffer@users.noreply.github.com>
2026-02-06 20:52:26 +00:00
jamesdanielwhitford
f413df8df7 feat: add OpenAI voice transcription setup to onboarding wizard (#191) 2026-02-06 12:09:33 -08:00
Cameron
b1d69965b5 feat: add <no-reply/> silent marker for agent opt-out (#196)
* 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>
2026-02-06 11:08:56 -08:00
Ari Webb
04f58e72c8 feat: add ergonomic channel management CLI (#188)
* feat: add ergonomic channel management CLI

Add `lettabot channels` command for easier channel management:
- `lettabot channels` - Interactive menu
- `lettabot channels list` - Show status of all channels
- `lettabot channels add <channel>` - Add with focused setup
- `lettabot channels remove <channel>` - Remove/disable
- `lettabot channels enable/disable <channel>` - Quick toggle

This makes it much easier to add a single channel without going
through the full onboard wizard. For example, adding Discord
after already having Telegram configured now only requires
the Discord-specific prompts.

Also fixes test for /reset command (was added but test not updated).

🐙 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

* refactor: remove enable/disable commands from channels CLI

Simplify the channels CLI to just add/remove. The enable/disable
commands were redundant - users can use `add` to reconfigure
and `remove` to disable.

🐙 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

* clean up

---------

Co-authored-by: Letta <noreply@letta.com>
2026-02-06 10:58:09 -08:00
Gabriele Sarti
b1e1b5693c feat: make polling interval configurable via lettabot.yaml (#181)
Add `pollIntervalSec` to the Google integration config so the email
polling interval can be set in lettabot.yaml instead of requiring
the POLLING_INTERVAL_MS environment variable.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 10:50:22 -08:00
Gabriele Sarti
8cd48d9f54 fix: abort agent when stuck in tool-call loop (#185)
Add a configurable maxToolCalls safeguard (default: 100) that aborts the
session when the agent enters an infinite tool-calling loop. The stream
watchdog didn't catch this because the stream was active (sending
tool_call events), just not productive.

Configurable via lettabot.yaml:
  features:
    maxToolCalls: 100

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 10:43:37 -08:00
Cameron
0d32e05906 fix: orphaned approval recovery, empty-result retry, deploy stability (#183)
Fixes #180. See #194 for full analysis.
2026-02-06 10:42:41 -08:00
Cameron
5dea82acc5 fix: pin baileys to 6.7.21 and fix stale command count test (#193)
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."
2026-02-06 10:30:04 -08:00
Cameron
2373dbb3b1 fix: add npm overrides for keyv resolution (#162)
* fix: telegram ESM compatibility and improved diagnostics

- 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

* fix: add npm overrides for keyv resolution

Users were still hitting ERR_MODULE_NOT_FOUND for keyv even after
PR #154 added it as a direct dependency. This happens because npm's
hoisting doesn't always resolve peer deps of optional deps properly.

npm overrides force the package manager to use our root keyv version
for all nested references, which is the idiomatic solution for
transitive peer dependency issues.

Also adds troubleshooting entry to README.

Written by Cameron ◯ Letta Code

"The best error message is the one that never shows up."
- Thomas Fuchs
2026-02-06 10:25:30 -08:00
dependabot[bot]
49db36f52f chore(deps): bump @letta-ai/letta-client from 1.7.7 to 1.7.8 (#186)
Bumps [@letta-ai/letta-client](https://github.com/letta-ai/letta-node) from 1.7.7 to 1.7.8.
- [Release notes](https://github.com/letta-ai/letta-node/releases)
- [Changelog](https://github.com/letta-ai/letta-node/blob/main/CHANGELOG.md)
- [Commits](https://github.com/letta-ai/letta-node/compare/v1.7.7...v1.7.8)

---
updated-dependencies:
- dependency-name: "@letta-ai/letta-client"
  dependency-version: 1.7.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-06 10:25:27 -08:00
Cameron
3ff33fee87 fix: client-side defensive recovery for orphaned approval_request_messages (#182)
When a conversation has an orphaned approval_request_message from a
cancelled/failed run, every subsequent message fails with 409 CONFLICT.
The existing attemptRecovery() can't find these because it checks
agent.pending_approval (null) and scans runs with stop_reason=requires_approval
(but the orphaned run is cancelled/failed).

Adds recoverOrphanedConversationApproval() which directly inspects conversation
messages, finds unresolved approval requests, verifies the originating run is
terminated, and sends denials to clear the stuck state. Both processMessage()
and sendToAgent() now catch CONFLICT errors and retry once after recovery.

Fixes #180

Written by Cameron ◯ Letta Code

"It is not the strongest of the species that survives, nor the most intelligent,
but the one most responsive to change." - Charles Darwin
2026-02-05 17:46:44 -08:00
Cameron
3b7150013c fix: approval detection missing include param + /reset command + startup check (#175)
getPendingApprovals() was calling agents.retrieve() without the
include: ['agent.pending_approval'] parameter, so the Letta API never
returned the pending_approval field. This caused stuck server-side tool
approvals (requires_approval=true) to go undetected, leaving the agent
permanently stuck with empty responses.

Also adds:
- Proactive ensureNoToolApprovals() on startup to disable requires_approval
- /reset slash command for deployed instances (clears conversation, keeps memory)
- Robust parsing for ToolCallDelta and deprecated tool_call field formats

Written by Cameron ◯ Letta Code

"The only way to do great work is to love what you do." - Steve Jobs
2026-02-05 17:38:48 -08:00
Cameron
0bed2cc166 fix: replace deprecated discord.js 'ready' event with 'clientReady' (#178)
The 'ready' event is deprecated in discord.js v14 and will break in v15.
Also switch from on() to once() since this event fires only once per login.

Reported-by: Aeo

Written by Cameron ◯ Letta Code

"The quietest fix is the one that prevents the loudest crash." -- a wise linter
2026-02-05 16:19:47 -08:00
Cameron
002fa48b2a docs: add releasing guide (#177)
Documents the automated release workflow:
- How to tag and push releases
- Pre-release detection (alpha/beta/rc)
- Versioning strategy (semver)
- Release checklist
- Links to npm publish tracking issue (#174)

Written by Cameron ◯ Letta Code

"Ship it." - GitHub
2026-02-05 15:13:19 -08:00
Cameron
610f2a64eb fix: remove broken letta-code notification step from release workflow (#176)
The `if: ${{ secrets.LETTA_API_KEY != '' }}` expression caused a
workflow parse error (secrets can't be used in `if` conditions).
Also `custom_prompt` may not be a valid input for letta-code-action.

Stripped the step for now -- will re-add once letta-code-action
supports release events.

Written by Cameron ◯ Letta Code

"Simplicity is the ultimate sophistication."
- Leonardo da Vinci
2026-02-05 15:07:40 -08:00
Cameron
9bd0134f72 feat: add automated GitHub Release workflow (#173)
Triggers on version tags (v*). Workflow:
1. Builds and runs tests (gate)
2. Generates release notes from merged PRs since last tag
3. Creates GitHub Release (with pre-release detection for alpha/beta/rc)
4. Optionally pings letta-code agent to write a friendly summary

Usage:
  git tag v0.1.0
  git push origin v0.1.0

Written by Cameron ◯ Letta Code

"Release early, release often."
- Eric S. Raymond
2026-02-05 14:58:01 -08:00
Cameron
4bb1e584cf fix: WhatsApp self-chat detection + debug logging (#171)
Fixes silent message drops for selfChat users by fixing an
inconsistency between two self-chat detection mechanisms:

1. `isSelfChatMessage()` in utils.ts correctly detects LID-based
   self-chat (common on newer WhatsApp versions)
2. `isExtractedSelfChat` in extract.ts only checked `from === selfE164`,
   missing LID-based self-chat entirely

When these disagreed, the fromMe filter would pass but the
selfChatMode filter would drop the message silently.

Also adds DEBUG_WHATSAPP=1 environment variable for verbose logging
at all message filter points - helps diagnose similar issues.

Written by Cameron ◯ Letta Code

"Debugging is twice as hard as writing the code in the first place."
- Brian Kernighan
2026-02-05 13:58:17 -08:00
Cameron
7db7f35804 feat: standardize message envelopes around <system-reminder> XML tags (#172)
Replace bracket-based message envelopes with XML system-reminder tags
matching Letta Code CLI conventions. The agent already recognizes these
tags, so this standardizes how lettabot sends metadata.

Changes:
- Rewrite formatMessageEnvelope() to use <system-reminder> tags
- Add Session Context section for first message in new chat sessions
- Add Chat Context section (group info, mentions, reply context)
- Move format hints into structured metadata fields
- Export SYSTEM_REMINDER constants and SessionContextOptions
- Update all 36 tests to validate new XML format + add session tests

Closes #170

Written by Cameron ◯ Letta Code

> "The best code is the code that speaks the same language everywhere." - Unknown
2026-02-05 13:54:44 -08:00
Cameron
c85c4a3272 fix: telegram ESM compatibility and improved diagnostics (#161)
- 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
2026-02-05 10:31:53 -08:00
Cameron
257da79e94 fix: allow selfhosted mode without LETTA_API_KEY (#160)
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
2026-02-05 10:27:33 -08:00
Cameron
407cd2f18d fix: improve empty response error message (no reset suggestion) (#159)
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
2026-02-05 09:43:58 -08:00
Cameron
1ff0aede9d fix: check for pending approvals before rejecting due to max attempts (#158)
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
2026-02-05 09:30:10 -08:00
Jason Carreira
4c860c748d Fix approval recovery and watchdog sendToAgent (#157)
Co-authored-by: Jason Carreira <jason@visotrust.com>
2026-02-05 08:09:51 -08:00
Cameron
d6113cab66 fix: graceful transcription fallback when ffmpeg unavailable (#155)
* 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
2026-02-04 19:31:50 -08:00
Cameron
b4058f17ce fix: add keyv as direct dependency (#154)
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
2026-02-04 18:43:10 -08:00
Cameron
63c6d60c05 fix: reset recovery counter + add skills loader tests (#153)
* 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
2026-02-04 18:41:32 -08:00
Cameron
8c4a472480 feat: add LETTABOT_CONFIG env var for config path (#152)
* 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
2026-02-04 18:36:10 -08:00
Cameron
9be59847f3 docs: add TESTING.md guide (#151)
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
2026-02-04 18:00:26 -08:00
Cameron
3d1f536c93 fix: MockChannelAdapter handles commands like real channels (#150)
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
2026-02-04 17:54:28 -08:00
Cameron
fe233b2f8f feat: add e2e tests with Letta Cloud (#149)
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)
2026-02-04 17:51:23 -08:00
Cameron
1113631252 feat(skills): install to agent-scoped location instead of .skills/ (#148)
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
2026-02-04 17:25:53 -08:00
Cameron
030a2b2bc5 feat: add CI test workflow and commands tests (#147)
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
2026-02-04 17:13:18 -08:00
Cameron
c8d55c8e84 feat: add Signal group chat support with mention gating (#146)
Add group message filtering so bot only responds when mentioned:

- Add native Signal mentions support (mentions array in SSE)
- Add quote/reply detection
- Add regex pattern matching (configurable via mentionPatterns)
- Add E.164 phone number fallback
- Add per-group config (groups.*.requireMention)
- Default: requireMention=true for safety (opt-in to respond to all)

Config example:
```yaml
channels:
  signal:
    mentionPatterns: ["@bot", "@lettabot"]
    groups:
      "*":
        requireMention: true
      "group:abc123":
        requireMention: false
```

Closes #137

Written by Cameron ◯ Letta Code

"In groups, listen first. Speak only when spoken to." - Bot wisdom
2026-02-04 17:11:23 -08:00