[IN TESTING — production on ani@wiuf.net, treat as experimental]
bot.ts — !reset aster cycles only Aster's conversation (leaves Ani's alone),
patches the systemd service file in place so restarts also use the new conv ID.
Full !reset now co-cycles Aster's conversation alongside Ani's so failure
notifications target the active context. Both commands write through to
lettabot-agent.json and daemon-reload immediately.
bot.ts — subagent thread results are now chunked at 16KB before posting to
Matrix threads. Previously truncated at 800 chars, cutting results mid-sentence.
store.ts / letta-api.ts — createConversationForAgent exposed for use by reset
commands. Store gains setAgentField for targeted JSON updates without clobbering.
config/types.ts, channels/factory.ts — conscience env var plumbing (CONSCIENCE_AGENT_ID,
CONSCIENCE_CONVERSATION_ID) wired through the config surface.
memfs-server.py — git sidecar for local memfs serving (port 8285). Serves bare
repos from ~/.letta/memfs/repository/ over HTTP. Required by letta-code memfs
in self-hosted mode.
Extracts a DisplayPipeline async generator that wraps the raw SDK stream
and yields clean DisplayEvent types. Refactors processMessage() to consume
pipeline events instead of raw StreamMsg objects.
- Locks foreground on first substantive event (reasoning/tool_call/etc),
eliminating buffering delay for real-time display
- Filters pre-foreground error/retry events to prevent false approval recovery
- Re-throws 429 in rejectApproval to prevent rate-limit loops
- Gates reasoning log on display config
- 12 pipeline unit tests + updated integration tests (56 total)
- Net -224 lines from bot.ts
Written by Cameron ◯ Letta Code
"The purpose of abstraction is not to be vague, but to create a new
semantic level in which one can be absolutely precise." -- Edsger Dijkstra
* 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
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>
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
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
* 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
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)
* feat: add Railway deployment support with agent auto-discovery
- Add railway.toml for build/deploy config with health checks
- Skip config file requirement when RAILWAY_ENVIRONMENT detected
- Auto-discover existing agent by name on container deploys
- Add findAgentByName() API function for agent lookup
- Add setAgentId() method to LettaBot class
- Add comprehensive Railway deployment docs
One-click deploy flow:
1. Set LETTA_API_KEY + channel tokens
2. LettaBot finds existing agent by AGENT_NAME (default: "LettaBot")
3. If not found, creates on first message
4. Subsequent deploys auto-reconnect to same agent
Written by Cameron ◯ Letta Code
"The best way to predict the future is to deploy it." - Railway, probably
* fix: specify Node 22 for Railway deployment
* fix: fail fast if LETTA_API_KEY is missing
* fix: don't await Telegram bot.start() - it never resolves
* fix: extract message from send_message tool call
* Revert "fix: extract message from send_message tool call"
This reverts commit 370306e49de3728434352d2df1b78c744e888833.
* fix: clear LETTA_AGENT_ID env var when agent doesn't exist
* docs: add Railway deploy button to README and docs
* fix: .nvmrc newline and correct MODEL default in docs
Add "X-Letta-Source: lettabot" header to Letta API client
for usage tracking/telemetry.
Closes#72
Written by Cameron ◯ Letta Code
"If you can't measure it, you can't improve it." - Peter Drucker