Commit Graph

89 Commits

Author SHA1 Message Date
Cameron
9975568831 feat(core): proactive approval detection via SDK bootstrapState() (#383) 2026-02-24 11:58:44 -08:00
Cameron
b5e44e493a fix: skip retry/recovery for non-retryable API errors (#384) 2026-02-24 11:47:36 -08:00
Jason Carreira
69b3d165d6 Add per-channel conversation overrides (#340)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Cameron <cameron@pfiffer.org>
2026-02-24 11:14:26 -08:00
Cameron
266ded012c fix: custom tool call display with pretty verb headers (#369) 2026-02-24 10:45:04 -08:00
Cameron
1cad6e6508 feat(core): structured logging with pino (#368) 2026-02-23 16:35:23 -08:00
Cameron
1ce869e5b7 fix(core): gate directive execution in listening mode and anchor paths to workingDir (#371) 2026-02-23 16:28:54 -08:00
Jason Carreira
1fbd6d5a2e Add send-file directive and Discord/CLI file support (#319)
Co-authored-by: Jason Carreira <jason@visotrust.com>
Co-authored-by: Cameron <cameron@pfiffer.org>
Co-authored-by: Charles Packer <packercharles@gmail.com>
Co-authored-by: Sarah Wooders <sarahwooders@gmail.com>
Co-authored-by: Letta <noreply@letta.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 15:44:34 -08:00
Cameron
7028f042af fix: display-reasoning stream fixes and Signal compatibility (#355) 2026-02-23 15:00:17 -08:00
Cameron
4fa212a9a1 fix: harden agent store persistence and startup agent discovery (#357) 2026-02-23 12:06:53 -08:00
Cameron
0544102fe3 feat: display tool calls and reasoning in channel output (#302) 2026-02-23 09:15:43 -08:00
Cameron
a06641b08d feat: surface meaningful API error messages to users (#350) 2026-02-23 00:11:04 -08:00
github-actions[bot]
a3c944bd13 feat: expose memfs (memory filesystem) option in lettabot config and SDK session (#336)
Adds features.memfs config key that controls whether the Letta Code CLI
receives --memfs or --no-memfs when creating/resuming SDK sessions. This
enables lettabot users to opt into git-backed memory filesystem (context
repositories) for persistent local memory sync.

- Config types: memfs?: boolean on AgentConfig.features, LettaBotConfig.features, BotConfig
- Bot wiring: baseSessionOptions() and createAgent() pass memfs to SDK when defined
- Main wiring: YAML config takes precedence, LETTABOT_MEMFS env var as fallback
- Legacy fix: conversations passthrough in single-agent normalization
- Tests: 3 memfs wiring tests (true/false/undefined), 2 conversations passthrough tests
- Docs: configuration.md section with known limitations, example YAML

Fixes #335

Written by Cameron and Letta Code

"The best way to predict the future is to implement it." -- David Heinemeier Hansson
2026-02-22 05:37:01 +01:00
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
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
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
39d2667667 feat: AskUserQuestion channel flow support (#272) 2026-02-10 17:58:47 -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
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
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
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
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
Cameron
e3df025bd8 refactor: unify bot loop with runSession(), drop initialize/timeout (#238)
* refactor: unify bot loop with runSession(), drop initialize/timeout

Unify the duplicated session lifecycle in processMessage() and
sendToAgent() into shared helpers:

- baseSessionOptions: computed once, not duplicated
- getSession(): 3-way create/resume/fallback in one place
- persistSessionState(): save agentId/conversationId/skills
- runSession(): send with CONFLICT retry, deduplicated stream

Also:
- Drop session.initialize() -- SDK auto-initializes on send()
- Drop withTimeout() wrapper -- SDK should own timeouts
- sendToAgent() shrinks from 98 to 20 lines
- processMessage() shrinks from 437 to ~250 lines (delivery stays)
- Net -187 lines (1013 -> 825)

All recovery logic preserved: pre-send attemptRecovery(),
CONFLICT catch + retry, empty-result orphan recovery.

Fixes #197

Written by Cameron ◯ Letta Code

"Make it work, make it right, make it fast." -- Kent Beck

* fix: narrow conversation-not-found fallback to 404/missing errors

Codex review caught that runSession() was retrying with createSession()
on ANY send error when agentId exists, not just conversation-missing
cases. Auth/network/protocol errors would incorrectly fork conversations.

Now only retries on 404 or error messages containing "not found" /
"missing" / "does not exist". Other errors propagate immediately.

Written by Cameron ◯ Letta Code

"Be conservative in what you send, be liberal in what you accept." -- Postel's Law

* fix: persist agent ID eagerly on creation, not deferred to result

Codex review caught that agent/conversation IDs were only saved in the
stream result handler. If createAgent() succeeded but send/stream failed,
the ID was lost and the next message would create a duplicate agent.

Now: getSession() persists the agent ID + runs first-run setup (name,
skills) immediately after createAgent(). persistSessionState() only
updates conversation ID on result.

Written by Cameron ◯ Letta Code

"Fail fast, but don't forget what you learned." -- unknown

* fix: persist conversation ID after send, before streaming

Codex review caught that conversationId was only saved on stream result.
If streaming disconnected or aborted before result, the next turn would
fall back to resumeSession(agentId) (default conversation) instead of
resuming the actual conversation -- forking context.

Now saved immediately after successful send(), matching the pre-refactor
behavior where it was saved after initialize().

Written by Cameron ◯ Letta Code

"The best time to save state was before the failure. The second best time is now." -- adapted proverb
2026-02-09 15:19:13 -08:00
Cameron
2c2ecbce9b fix: switch group batching from fixed timer to 5-second debounce (#237)
The old 10-minute fixed timer caused groups to feel unresponsive after
inactivity. Now uses debounce: timer resets on every new message, flushes
after 5 seconds of quiet. @mentions still flush immediately.

New config: groupDebounceSec (default 5). Old groupPollIntervalMin still
works (converted to ms) for backward compatibility.

Fixes #229

Written by Cameron ◯ Letta Code

"The user is always right. If there is a problem with the use of the system, it's the system that's wrong, not the user." -- Don Norman
2026-02-09 11:11:24 -08:00
Cameron
4effadccb9 merge: resolve conflict with main (multi-agent architecture)
- Keep multi-agent normalizeAgents() flow from main
- Integrate deprecation warning for agent.model from PR
- Remove model from LettaBot constructor (server-side property)
- Remove Model: display from single-agent startup log

Written by Cameron ◯ Letta Code

"The best interface is no interface." -- Golden Krishna
2026-02-09 10:52:32 -08:00
Cameron
1a381757bb fix: Telegram messages truncated when MarkdownV2 edit fails (#236)
editMessage() had no fallback for MarkdownV2 failures (unlike sendMessage
which already falls back to plain text). When the agent generates markdown
tables or other complex formatting, the MarkdownV2 conversion can fail
mid-stream, silently leaving the user with whatever the last successful
streaming edit was -- a truncated message.

Three fixes:
- editMessage() now mirrors sendMessage's try/catch with plain-text fallback
- Final send retry no longer guarded by !messageId, so failed edits fall
  back to sending a new complete message
- Streaming edit errors are logged instead of silently swallowed

Written by Cameron ◯ Letta Code

"If you want to go fast, go alone. If you want to go far, go together." - African Proverb
2026-02-09 10:47:54 -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
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
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
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
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
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
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
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