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
Address review findings from self-review and codex:
- Commands (/reset, /cancel) now receive forcePerChat from the Discord
adapter and resolve the correct per-thread conversation key
- Group batcher propagates forcePerChat to synthetic batch messages so
debounced thread messages don't fall back to shared routing
- Reaction handler sets forcePerChat for thread-only reactions
- Session LRU eviction fires regardless of conversationMode so
forcePerChat sessions don't accumulate without bounds
- Docs now correctly state thread-only overrides shared/per-channel
modes (not disabled mode)
Written by Cameron ◯ Letta Code
"The best way to predict the future is to implement it." -- David Heinemeier Hansson
Thread-only mode previously relied on the global conversationMode to
determine whether threads got separate conversations. In shared or
per-channel mode, all threads shared one conversation causing crosstalk.
Add forcePerChat flag on InboundMessage that the Discord adapter sets
when thread-only mode is active. resolveConversationKey treats flagged
messages as per-chat regardless of the configured mode, giving each
thread its own isolated conversation while still sharing agent memory.
Written by Cameron ◯ Letta Code
"Time is an illusion. Lunchtime doubly so." -- Douglas Adams
When sendToAgent() has no valid sourceChannel (e.g. webhook/feed
triggers), filter directives to only those that carry explicit
channel/chat targets. Prevents non-targeted directives (react, voice,
untargeted send-file) from executing against an arbitrary first-registered
adapter.
Written by Cameron ◯ Letta Code
"Simplicity is prerequisite for reliability." -- Edsger Dijkstra
Two fixes from PR review:
1. sendToAgent() now parses and executes directives from agent responses.
Previously, directives were only executed in processMessage() (foreground),
so <send-message> and targeted <send-file> never fired from heartbeats,
cron, or webhook contexts. Targeted directives resolve their own adapter;
non-targeted directives use source context from the trigger if available.
2. send-file with only one of channel/chat (partial targeting) is now
rejected with a warning instead of silently falling back to the
triggering chat, which could send to an unintended destination.
Written by Cameron ◯ Letta Code
"Be conservative in what you send, be liberal in what you accept."
-- Jon Postel
Adds a new `<send-message>` directive that lets the agent proactively
send text messages to any connected channel:chat, and extends
`<send-file>` with optional `channel`/`chat` attributes for targeted
file delivery. Both work from any context including heartbeats and cron.
Written by Cameron ◯ Letta Code
"The question of whether a computer can think is no more interesting
than the question of whether a submarine can swim." -- Edsger Dijkstra