Commit Graph

207 Commits

Author SHA1 Message Date
Jason Carreira
38428c1e7c fix: parse directives before checking no-reply marker (#338)
Fixes reaction-only responses triggering spurious error messages. The <no-reply/> check now runs after directive parsing in both finalizeMessage() and the post-stream handler.

Fixes the case where <actions><react emoji="..." /></actions> as the full response would add the reaction but also send an error message.

Written by Cameron and Letta Code

"First, solve the problem. Then, write the code." -- John Johnson
2026-02-21 12:37:02 +01:00
Cameron
d4c3d32a8d fix: replace unhelpful error messages with actionable guidance (#332) 2026-02-21 12:17:12 +01:00
Cameron
6cc59708ff fix(core): harden session lifecycle around init/send failures (#333) 2026-02-21 12:16:58 +01:00
Cameron
83da7069cf revert: remove unreviewed multi-agent routing scaffold (#330) 2026-02-18 14:57:50 +01:00
Sarah Wooders
44c5a70761 fix(core): restore gateway compatibility and unblock build (#327)
Co-authored-by: Letta <noreply@letta.com>
2026-02-17 18:58:55 -08:00
Sarah Wooders
60f70089a6 feat(core): add multi-agent routing foundation
Add config normalization, routing bindings, and per-agent instance management to support channel/account-aware dispatch with default agent fallback.

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

Co-Authored-By: Letta <noreply@letta.com>
2026-02-17 17:34:48 -08:00
Charles Packer
d85a836ba7 test: lock SDK session reuse contract for follow-up sends (#314) 2026-02-16 19:52:17 -08:00
github-actions[bot]
b6bfd14cd9 fix: wire up cron job --deliver flag to actually deliver responses (#290)
Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com>
Co-authored-by: Cameron <cpfiffer@users.noreply.github.com>
Co-authored-by: Cameron <cameron@pfiffer.org>
2026-02-13 18:10:24 -08:00
Cameron
6ef987a04f fix: modernize onboarding group settings with unified modes (#296) 2026-02-13 17:59:40 -08:00
Cameron
c083638be1 feat: remote pairing approval via API (#301) 2026-02-13 17:35:56 -08:00
Cameron
560380d721 feat: per-channel conversation routing (#299) 2026-02-13 17:21:38 -08:00
Cameron
09ce3b810f fix: listen mode streaming leak + receiveBotMessages for Discord (#295) 2026-02-12 18:57:13 -08:00
Cameron
add73bdb51 feat: show version and commit hash in startup banner (#293) 2026-02-12 18:49:20 -08:00
Cameron
296db3a858 fix: expose WhatsApp/Signal connection fields in per-agent config (#294) 2026-02-12 18:40:46 -08:00
John Wessel
d0a34d209e Auto-bind to 0.0.0.0 on container platforms for health checks (#291)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:38:47 -08:00
Cameron
014c2b56aa fix: read heartbeat target from per-agent config (#292) 2026-02-12 18:36:25 -08:00
Cameron
b79d705a99 perf: reuse SDK session subprocess across messages (#289) 2026-02-12 11:24:58 -08:00
Cameron
01ed38a15d feat: per-agent todo system with heartbeat integration (#288) 2026-02-12 10:23:14 -08:00
Cameron
dcd428d598 fix: improve error logging for failed agent runs (#287) 2026-02-11 15:42:17 -08:00
Cameron
de216ea3ff fix: TS compilation errors from #283 squash merge (#286) 2026-02-11 15:24:48 -08:00
Cameron
c405c96c9d feat: add per-group allowedUsers filtering for all channels (#283) 2026-02-11 15:20:01 -08:00
Cameron
9550fc0c03 Fix cron-jobs path mismatch between CronService and lettabot-schedule (#284) 2026-02-11 14:04:26 -08:00
Cameron
046f15feaa fix: centralize strict config loading and fail fast on invalid config (#280) 2026-02-11 10:47:16 -08:00
Cameron
c0f82ccd63 Merge remote-tracking branch 'origin/fix/server-api-config' into HEAD
# Conflicts:
#	docs/configuration.md
#	src/config/io.ts
#	src/main.ts
2026-02-10 20:39:54 -08:00
Charles Packer
de1adcf4fe fix: fix server terminology with mode aliases (#277) 2026-02-10 20:34:29 -08:00
Cameron
c4c8b17018 fix: support server.api config, preserve api across onboard, fix load error logging
Three bugs fixed:

1. server.api is now the canonical location for API server config (port,
   host, CORS). Users naturally nest api under server -- this now works.
   Top-level api still accepted with a deprecation warning.

2. Onboarding no longer silently drops api and attachments config when
   saving. Both interactive and non-interactive paths now preserve
   unmanaged top-level fields from the existing config.

3. When YAML parsing fails, the log no longer misleadingly says
   "Loaded from lettabot.yaml". A didLoadFail() flag enables accurate
   status reporting without changing 17+ loadConfig() call sites.

Written by Cameron ◯ Letta Code

"The map is not the territory, but a good map sure helps you find port 6702."
2026-02-10 20:16:36 -08:00
Cameron
83569d968e fix: add LETTA_AGENT_NAME env var and improve Railway docs (#275) 2026-02-10 19:55:16 -08:00
Cameron
9d684b6a69 fix: resolve TS2349 build error in ask-user-question test (#274) 2026-02-10 19:53:34 -08:00
Cameron
358339b76b fix: clarify system prompt to prevent duplicate message delivery (#269) 2026-02-10 19:47:13 -08:00
Cameron
39d2667667 feat: AskUserQuestion channel flow support (#272) 2026-02-10 17:58:47 -08:00
Cameron
57f102dfd4 feat: add loom ASCII art startup banner (#270) 2026-02-10 17:57:19 -08:00
ghosttigerllc-bit
28adc22388 feat: add telegram-mtproto channel for user account messaging (#189)
Co-authored-by: Kai <noreply@gtb.ai>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cameron <cameron@pfiffer.org>
2026-02-10 17:25:44 -08:00
Charles Packer
c7cb3b723b Merge pull request #260 from letta-ai/feat/disallow-interactive-plan-tools
feat: add disallowed interactive tools config for sessions
2026-02-10 16:20:33 -08:00
Cameron
c410decd18 feat: unified group modes (open/listen/mention-only) (#267)
Consolidates listeningGroups and groups.requireMention into a single
groups config with explicit mode per group. Backward compatible --
legacy formats auto-normalize with deprecation warnings.

- Add shared group-mode.ts with isGroupAllowed/resolveGroupMode helpers
- Update all 5 channel adapters to use mode-based gating
- Default to mention-only for configured entries (safe), open when no config
- Listening mode now set at adapter level, bot.ts has legacy fallback
- Fix YAML large-ID parsing for groups map keys (Discord snowflakes)
- Add migration in normalizeAgents for listeningGroups + requireMention
- Add unit tests for group-mode helpers + update all gating tests
- Update docs, README, and example config

Closes #266

Written by Cameron and 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-10 16:01:21 -08:00
Cameron
745291841d feat: default new configs to multi-agent format (#261)
* feat: default new configs to multi-agent format

Onboarding and non-interactive config generation now emit the
agents[] array format instead of the legacy agent/channels/features
flat structure. This makes adding a second agent a simple array
append rather than a config format migration.

Existing legacy configs continue to work -- normalizeAgents()
handles both formats at runtime.

Written by Cameron and Letta Code

"The future is already here -- it's just not very evenly distributed." -- William Gibson

* test: add save/load roundtrip tests for agents[] config format

Tests the actual YAML serialization path:
- agents[] written without legacy agent/channels at top level
- roundtrip through save + load + normalizeAgents preserves all fields
- global fields (providers, transcription) stay at top level

Written by Cameron and Letta Code

"Programs must be written for people to read, and only incidentally for machines to execute." -- Abelson & Sussman

* feat: eagerly create agent during onboarding

When the user picks "Create new agent", the agent is now created
immediately (with spinner) rather than deferred to first message.
The agent ID is written directly to lettabot.yaml, making the
config file the single source of truth.

Creates the agent with system prompt, memory blocks, selected model,
display name, skills, and disables tool approvals -- same setup that
bot.ts previously did lazily on first message.

Graceful fallback: if creation fails, falls back to lazy creation.

Written by Cameron and Letta Code

"Make it work, make it right, make it fast." -- Kent Beck
2026-02-10 14:58:46 -08:00
Cameron
e8a97a2cbb fix: add stream debug visibility for tool calls and UUID gap detection (#264)
Tool call events between assistant messages were invisible in [Stream]
debug output, making it hard to understand message finalization timing.

- Add sawNonAssistantSinceLastUuid tracking to warn when assistant UUID
  changes with no visible tool_call/reasoning events in between
- Replace [Bot] tool logging with [Stream]-prefixed structured summaries
  (>>> TOOL CALL, <<< TOOL RESULT) for greppable output
- Bridge DEBUG=1 to DEBUG_SDK=1 so SDK-level dropped wire messages are
  visible when debugging

Written by Cameron ◯ Letta Code

"The stream does not resist the rock; it flows around it, revealing its shape." -- Unknown
2026-02-10 14:49:06 -08:00
Cameron
f5371a9ba7 feat: add group gating to Telegram, Discord, and Slack (#258) (#265)
WhatsApp and Signal already had groups config with requireMention and
group allowlists. This brings the same pattern to the remaining three
channels, giving operators consistent control over which groups the bot
participates in and whether mentions are required.

- New `groups` config for Telegram, Discord, Slack (per-group allowlist
  with requireMention, wildcard support)
- Telegram: standalone gating module with entity, text, command, and
  regex mention detection; applied to text, voice, and attachment handlers
- Discord: inline gating using native message.mentions API
- Slack: gating in message handler (drops non-mentions) and app_mention
  handler (allowlist check); helper methods on adapter
- Signal: pass wasMentioned through to InboundMessage (was detected but
  never forwarded)
- Config wiring: groups/mentionPatterns forwarded from main.ts to all
  adapter constructors
- 17 new tests for Telegram gating

Written by Cameron ◯ Letta Code

"Even in the group chat of life, sometimes you gotta be @mentioned
to know the universe is talking to you." — Ancient Internet Proverb
2026-02-10 14:47:19 -08:00
Jason Carreira
df43091d21 fix: CLI group settings handling and env var support (#257)
* Fix CLI group settings handling

* Document group settings in config

* fix: move group settings below channel list in README, add GROUP_DEBOUNCE_SEC env var

- README: group settings section was splitting the cross-channel bullet
  list; moved it after the channel table
- onboard.ts: groupDebounceSec had no env var override for non-interactive
  deploys; added <CHANNEL>_GROUP_DEBOUNCE_SEC for all 5 channels
- SKILL.md: documented the new env var

Written by Cameron and Letta Code

"For every complex problem there is an answer that is clear, simple, and wrong." - H.L. Mencken

---------

Co-authored-by: Jason Carreira <jason@visotrust.com>
Co-authored-by: Cameron <cameron@pfiffer.org>
2026-02-10 14:01:39 -08:00
cpacker
c1ca8a8080 feat: add disallowed interactive tools config for sessions 2026-02-10 13:21:58 -08:00
Cameron
df18cba565 feat: configurable displayName prefix for agent messages in group chats (#255)
Add optional displayName field to agent config. When set, outbound
agent responses are prefixed (e.g. "💜 Signo: Hello!").

Useful in multi-agent group chats where multiple bots share a channel
and users need to tell them apart.

Closes #252

Written by Cameron ◯ Letta Code

"The details are not the details. They make the design." -- Charles Eames
2026-02-10 12:08:45 -08:00
Cameron
dad510150a fix: prevent duplicate Telegram messages on "not modified" edit (#254)
* 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."
2026-02-10 11:53:51 -08:00
Jason Carreira
a4934a77d4 Fix action directives parsing and reactions (#248)
* 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>
2026-02-10 11:40:23 -08:00
Gabriele Sarti
0dbd743f7f feat: add listening mode for groups (#208)
Add `listeningGroups` config option (per-channel, same pattern as
`instantGroups`) where the bot periodically reads group messages and
sends them to the Letta agent for memory updates, but force-suppresses
response delivery unless the bot is explicitly mentioned.

- Add `isListeningMode` field to `InboundMessage`
- Add `listeningGroups` to all 5 channel configs
- Map `listeningGroups` to env vars in `configToEnv()`
- Rename `fixInstantGroupIds` to `fixLargeGroupIds` to also fix
  `listeningGroups` entries (Discord snowflake precision fix)
- Build `listeningGroupIds` set and pass to bot
- Tag messages as listening mode in `processGroupBatch()` when group
  is in listening mode and bot wasn't mentioned
- In `processMessage()`: skip typing indicator, heartbeat target
  update, recovery error messages, and force-suppress response
  delivery for listening mode messages
- Add `[OBSERVATION ONLY]` header to batch envelope for listening mode
- Document in lettabot.example.yaml

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 11:01:26 -08:00
Ari Webb
d52ea4e855 fix: create new conversation when no stored conversation ID exists (#245) (#250)
When an agent ID is configured but no conversation ID is stored, LettaBot
previously tried to resume the agent's default conversation via resumeSession().
If that default conversation was deleted or never created (e.g., after
migrations), the CLI exited with "Conversation default not found" and the bot
never replied to messages.

The fix changes getSession() to call createSession() instead of resumeSession()
when there's no stored conversation ID, ensuring a new conversation is always
created in this case.

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

Co-authored-by: Letta <noreply@letta.com>
2026-02-10 10:29:06 -08:00
Cameron
9c0228d414 fix: recover from active runs stuck on approval (#249)
* fix: recover from active runs stuck on approval

recoverOrphanedConversationApproval() only resolved approvals from
terminated (failed/cancelled) or abandoned (completed/requires_approval)
runs. Active runs with status=running and stop_reason=requires_approval
were skipped, even though they block the entire conversation.

This happens when the CLI's HITL approval flow leaves a pending approval
that no client will resolve (e.g. agent triggered via ADE, user walked
away). Every subsequent message fails with success=false.

Now also handles running+requires_approval: rejects the approval and
cancels the stuck run so lettabot can proceed.

Fixes the recovery path for all three call sites: pre-send check,
CONFLICT catch, and error-result retry.

Written by Cameron ◯ Letta Code

"The best error message is the one that never shows up." -- Thomas Fuchs

* fix: use cancelRuns return value for accurate diagnostics

Check boolean return from cancelRuns() instead of always logging
success. Details string now shows "(cancel failed)" when it didn't
work.

Written by Cameron ◯ Letta Code

"Trust, but verify." -- Ronald Reagan

* fix: detect terminal errors and retry with recovery in bot.ts

Monkey patch until SDK issue letta-code-sdk#31 is resolved.

1. Detect terminal errors (success=false OR error field), not just
   empty-success. Both trigger orphan recovery + retry.

2. Blind retry on terminal error when no orphaned approvals found --
   client-side approval failures may leave no detectable record.

3. sendToAgent() throws on terminal error instead of returning empty
   string. Background callers get actionable errors.

Written by Cameron ◯ Letta Code

"A good patch is one you can remove." -- unknown

* test: add tests for approval recovery including stuck runs

7 tests covering recoverOrphanedConversationApproval():
- Empty conversation
- No unresolved approvals
- Recovery from failed run
- Recovery from stuck running+requires_approval (with cancel)
- Already-resolved approvals skipped
- Healthy running run not touched
- Cancel failure reported accurately

Written by Cameron ◯ Letta Code

"Code without tests is broken by design." -- Jacob Kaplan-Moss

* fix: gate retry on sentAnyMessage to prevent duplicate delivery

Codex review caught that finalizeMessage() clears the response buffer
on type changes, so hasResponse could be false even when output was
already sent. This caused duplicate retries with potential side effects.

Now checks !sentAnyMessage (authoritative delivery flag) in addition
to !hasResponse before retrying.

Written by Cameron ◯ Letta Code

"Idempotency: the art of doing nothing twice." -- unknown
2026-02-09 21:16:52 -08:00
Tom Fehring
d12633b792 Convert standard markdown to Slack mrkdwn for Slack messages (#234)
* 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>
2026-02-09 16:59:46 -08:00
Cameron
f2ec8f60c2 feat: support multiple Gmail accounts for polling (#244)
Add multi-account Gmail polling with per-account seen tracking, updated
onboarding flow, and config/env resolution.

Based on jasoncarreira's work in #214, rebased onto current main and
cleaned up:
- parseGmailAccounts() extracted to polling/service.ts with 10 unit tests
- Per-account seen email tracking (Map<string, Set<string>>) with legacy
  migration from single-account format
- Onboarding supports multi-select for existing accounts + add new
- Config resolution: polling.gmail.accounts > integrations.google.accounts
  (legacy) > GMAIL_ACCOUNT env (comma-separated)
- GoogleAccountConfig type for per-account service selection
- Updated docs/configuration.md

Closes #214.

Written by Cameron ◯ Letta Code

"Good artists copy, great artists steal." - Pablo Picasso
2026-02-09 16:58:34 -08:00
Cameron
deb1c4532a feat: add POST /api/v1/chat endpoint for agent messaging (#242)
* feat: add POST /api/v1/chat endpoint for sending messages to agents

Adds an HTTP endpoint that accepts a JSON message, sends it to the
lettabot agent via sendToAgent(), and returns the agent's response.
This enables external systems (e.g. server-side tools in other agents)
to communicate with lettabot programmatically.

- Add ChatRequest/ChatResponse types
- Add AgentRouter interface extending MessageDeliverer with sendToAgent()
- Implement AgentRouter on LettaGateway with agent-name routing
- Add POST /api/v1/chat route with auth, validation, and JSON body parsing

Written by Cameron ◯ Letta Code

"The most profound technologies are those that disappear." -- Mark Weiser

* feat: add SSE streaming support to /api/v1/chat endpoint

When the client sends Accept: text/event-stream, the chat endpoint
streams SDK messages as SSE events instead of waiting for the full
response. Each event is a JSON StreamMsg (assistant, tool_call,
tool_result, reasoning, result). The result event signals end-of-stream.

- Export StreamMsg type from bot.ts
- Add streamToAgent() to AgentSession interface and LettaBot
- Wire streamToAgent() through LettaGateway with agent-name routing
- Add SSE path in chat route (Accept header content negotiation)
- Handle client disconnect mid-stream gracefully

Written by Cameron ◯ Letta Code

"Any sufficiently advanced technology is indistinguishable from magic." -- Arthur C. Clarke

* test+docs: add chat endpoint tests and API documentation

- 10 tests for POST /api/v1/chat: auth, validation, sync response,
  agent routing, SSE streaming, stream error handling
- 6 tests for gateway sendToAgent/streamToAgent routing
- Fix timingSafeEqual crash on mismatched key lengths (return 401, not 500)
- Document chat endpoint in configuration.md with sync and SSE examples
- Add Chat API link to docs/README.md index

Written by Cameron ◯ Letta Code

"First, solve the problem. Then, write the code." -- John Johnson
2026-02-09 16:53:31 -08:00
Cameron
6f5a322840 fix: clear WhatsApp typing indicator after response (#243)
WhatsApp's "typing..." indicator lingers for 15-25 seconds after the bot
finishes responding because there was no way to clear it. This adds
stopTypingIndicator() which sends a "paused" presence update to
immediately dismiss it.

- stopTypingIndicator?() added to ChannelAdapter interface (optional)
- WhatsApp adapter implements it via sendPresenceUpdate("paused")
- bot.ts calls it in the finally block after stream processing

Written by Cameron ◯ Letta Code

"First, solve the problem. Then, write the code." - John Johnson
2026-02-09 16:25:52 -08:00
Cameron
5f7cdd3471 feat: XML response directives via <actions> wrapper block (#239)
Agents can now include an <actions> block at the start of their text
response to perform actions without tool calls. The block is stripped
before the message is delivered to the user.

Example:
  <actions>
    <react emoji="thumbsup" />
  </actions>
  Great idea!
  → Sends "Great idea!", reacts with thumbsup

- New directives parser (src/core/directives.ts) finds <actions> block
  at response start, parses self-closing child directives inside it
- addReaction() added to ChannelAdapter interface (Telegram, Slack,
  WhatsApp already implement it)
- Streaming holdback covers the full <actions> block duration (prefix
  check + incomplete block detection), preventing raw XML from flashing
- Directive execution extracted to executeDirectives() helper (no
  duplication between finalizeMessage and final send paths)
- Message envelope includes Response Directives section so all agents
  learn the feature regardless of system prompt
- System prompt documents the <actions> block syntax
- 19 unit tests for parser and stripping

Significantly cheaper than the Bash tool call approach (lettabot-react)
since no tool_call round trip is needed.

Relates to #19, #39, #240. Subsumes #210.

Written by Cameron ◯ Letta Code

"The best code is no code at all." - Jeff Atwood
2026-02-09 15:53:10 -08:00