## File: README.md # Letta Code [](https://www.npmjs.com/package/@letta-ai/letta-code) [](https://discord.gg/letta) Letta Code is a stateful agent harness for creating agents that are more like people than tools. Letta Code agents have memory, identity, and a sense of experience over time. They learn and evolve over long horizons through rewriting their own memory, skills, prompts, and even the harness itself (through mods). Letta Code can be used interactively, or to power always-on agents that work proactively. Interact with agents through: * A local [**CLI**](https://docs.letta.com/letta-code/cli) * The [**desktop app**](https://docs.letta.com/letta-code/desktop-app) for macOS, Windows, and Linux * Your browser, including [mobile](https://docs.letta.com/letta-code/remote-mobile), at [chat.letta.com](https://chat.letta.com) * Messaging integrations, including [Telegram](https://docs.letta.com/letta-code/channels#telegram-cli), [Slack](https://docs.letta.com/letta-code/channels#slack-cli), [Discord](https://docs.letta.com/letta-code/channels#discord-cli), and [custom channels](https://github.com/letta-ai/letta-code/blob/main/src/channels/README.md) ## Feature Overview > [!TIP] > Letta Code agents are designed to be self-configuring. If you want to configure something (e.g. skills, behavior, hooks, permissions), try asking your agent to do it for you. | Feature | Description | |---|---| | [Self-improvement & Learning](https://docs.letta.com/letta-code/memory) | Agents programmatically rewrite their context to improve and adapt over time, including system prompt learning (through [memory blocks](https://www.letta.com/blog/memory-blocks)) and [skill learning](https://www.letta.com/blog/skill-learning). Configure periodic dreaming with `/sleeptime`, audit memory quality with `/doctor`, and view memory with `/palace` | | [Message search](https://docs.letta.com/letta-code/slash-commands) | Search across all messages and agents with `/search`. Agent can also search their own conversations or the conversations of other agents | | [MemFS](https://docs.letta.com/letta-code/memfs) | All context (including memory blocks) is tracked via git. Sync context to a custom GitHub repository by setting `/memory-repository set git@github.com:...` | | [Skills](https://docs.letta.com/letta-code/skills) | Loads global skills (`~/.letta`), project-scoped skills (`.agents/skills`), and agent-scoped skills (stored in MemFS). View skills with `/skills` and create with `/skill-creator` | | [Subagents & Multi-agent](https://docs.letta.com/letta-code/subagents) | Call built-in subagents (general-purpose, forked, recall, history-analyzer) async or sync. Agents can call any other agent (including themselves) as subagents | | [Messaging Integrations](https://docs.letta.com/letta-code/channels) | Chat with the same agent from Slack, Telegram, your browser (chat.letta.com) including mobile, and through [custom channels](https://github.com/letta-ai/skills/blob/main/letta/creating-letta-code-channels/SKILL.md) | | [Hooks](https://docs.letta.com/letta-code/hooks) | Run custom scripts at key points of agent execution to automate workflows | | [Permissions](https://docs.letta.com/letta-code/permissions) | Set permission modes and customize what actions are auto-approved or auto-denied | | [Crons & Schedules](https://docs.letta.com/letta-code/scheduling) | Configure heartbeats and crons, and let agents work across time with self-managed schedules | | [Remote & Multi-Env](https://docs.letta.com/letta-code/client-server-architecture) (requires signing in with Letta) | Agents work across multiple environments. Make any machine available as a remote environment by running `letta server --env-name "..."` | | [Secrets](https://docs.letta.com/letta-code/secrets) (requires signing in with Letta) | Make secrets available as environment variables (across machines) while obfuscating their values from context | See the full list of slash commands in our [documentation](https://docs.letta.com/letta-code/slash-commands). ## Get started Install the package via [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm): ```bash npm install @letta-ai/letta-code ``` Navigate to your project directory and run `letta` (see command-line options [in the docs](https://docs.letta.com/letta-code/commands)). You can also run the tutorial agent with: ``` letta --new-agent --personality tutorial ``` Run `/connect` to configure your own LLM API keys (OpenAI / ChatGPT, Anthropic, Z.ai coding plan, etc.), and use `/model` to swap models. You can also download the [**desktop app**](https://docs.letta.com/letta-code/desktop-app) for macOS, Windows, and Linux. Agents created in the CLI are available via the desktop app, and vice versa. ## Letta Cloud Agents stored in Letta Cloud keep their memory, identity, and conversations there while the Letta Code harness can run on any connected computer: your laptop, [GitHub Actions](https://github.com/letta-ai/letta-code-action), a managed cloud sandbox, a remote VM, or a Mac Mini. You can chat with the same agents through [chat.letta.com](https://chat.letta.com/) or the desktop app. ```mermaid graph TD LettaCloud["Letta CloudAgent state"] LettaCloud --> A["πŸ’» Your Laptop"] LettaCloud --> B["☁️ Cloud VM"] LettaCloud --> C["πŸ–₯️ Mac Mini"] LettaCloud --> D["πŸ“¦ Managed Sandbox"] ``` Run `/login` from the CLI or sign in through the desktop app to access agents in your Letta account. ### Remote environments Agents stored in Letta Cloud can run across multiple machines. Any machine can be made into an available environment by running: ```bash letta server letta server --env-name "work-laptop" ``` List discoverable environments from the CLI: ```bash letta environments list --online-only ``` Get the current environment for routing another agent onto this same machine: ```bash letta environments current ``` Route a headless message through a specific environment: ```bash letta -p --agent --environment "work-laptop" "hello from that machine" ``` Use `--environment cloud` to start or reuse the target agent's cloud sandbox. Agent-to-agent headless messages without `--environment` keep the original same-environment behavior. See our guides for using [Railway](https://docs.letta.com/letta-code/remote#railway), [DigitalOcean](https://docs.letta.com/letta-code/remote#digitalocean), and [Fly.io](https://docs.letta.com/letta-code/remote#flyio) as remote environments. ## Installing external skills Install skills into a specific agent's memory with `letta skills install `: | Source | Example | |---|---| | GitHub | `letta skills install https://github.com/owner/repo``letta skills install https://github.com/owner/repo/tree/main/path/to/skill``letta skills install https://github.com/owner/repo/blob/main/path/to/skill/SKILL.md` | | [ClawHub](https://clawhub.ai/) | `openclaw skills install ` β†’ `letta skills install ` | | [Hermes Skills Hub](https://hermes-agent.nousresearch.com/docs/skills/) | `hermes skills install ` β†’ `letta skills install ` | To view skills run `letta skills list --agent `, and delete skills with `letta skills delete --agent `. ## Research Letta Code is developed by the creators of [MemGPT](https://arxiv.org/abs/2310.08560) and [sleep-time compute](https://arxiv.org/abs/2504.13171) (now called "dreaming"), and driven by our [research](https://www.letta.com/research) in AI memory and continual learning. ## Other Community maintained packages are available for Arch Linux users on the [AUR](https://aur.archlinux.org/packages/letta-code): ```bash yay -S letta-code # release yay -S letta-code-git # nightly ``` Nix users can run or install Letta Code through the repository flake: ```bash nix run github:letta-ai/letta-code nix profile install github:letta-ai/letta-code ``` See [docs/nix.md](docs/nix.md) for Home Manager and NixOS service examples. --- Made with πŸ’œ in San Francisco --- ## File: docs/plans/discord-channel-policy-consolidation.md # Discord channel policy consolidation plan This branch consolidates the overlapping Discord channel work from PR #2045 and PR #2306. ## Direction - Use PR #2306 as the architectural base for per-channel Discord behavior. - Keep the per-channel `allowed_channels` mode map instead of the account-level `channelPolicy` field from PR #2045. - Keep `thread_policy_by_channel`, delivery-time `allowed_channels` enforcement, `parentChannelId`, route reconciliation, and route cleanup gated by `remove_stale_routes`. - Preserve legacy-safe defaults unless a breaking behavior change is made deliberately in a later PR. - Port the useful orthogonal pieces from PR #2045: Discord audio transcription and setup-flow coverage for the new options. ## Keep from PR #2306 - `allowed_channels` as either the legacy `string[]` allowlist or a per-channel mode map of `open` / `mention-only`. - `thread_policy_by_channel` for per-channel mention-thread behavior. - Delivery-time route gating using `parentChannelId` so stale routes cannot bypass current channel policy. - `letta channels route reconcile --channel discord` with `--apply` guarded by `remove_stale_routes`. - Typing indicators for Discord while an agent response is in flight. - `acknowledge_message_reaction` as an explicit opt-in/out knob. - Snake-case persisted config migration for newly added account fields. ## Port from PR #2045 - Discord `transcribe_voice` support for inbound audio attachments. - Discord setup prompts for guild channel mode, mention thread behavior, debounce, reaction acknowledgments, and audio transcription. - Mention-preserving debounce semantics so an explicit mention in a buffered burst is not lost. ## Fix before opening the PR - Keep `autoThreadOnMention` defaulting to `false`; Discord thread creation is opt-in. - Keep `inboundDebounceMs` defaulting to `0` / disabled; open-channel debounce remains opt-in. - Do not overload `isMention` for open-channel routing; carry `isOpenChannel` separately. - Remove the unrelated Discord MessageChannel tool-result format change. - Add/update targeted tests for defaults, routing semantics, transcription, setup config, and websocket/config round trips. --- ## File: docs/plans/resume-tail-fast-path.md # Resume tail fast path plan ## Contract Boot/resume should use one small-tail algorithm for local and API backends: 1. Resolve the active agent and conversation using existing Letta CLI behavior. 2. If resuming, fetch a bounded tail for that exact conversation. 3. Convert that tail into TUI buffer lines. 4. Detect pending approvals from the literal tail. 5. Render the tail and show either the approval UI or the normal input. Default Letta behavior stays stateful: normal startup resumes the default/last conversation. ## Backend boundary Add a backend-level resume-tail operation, conceptually: ```ts type ResumeTail = { messages: Message[]; pendingApprovals: ApprovalRequest[]; }; getConversationResumeTail(agentId: string, conversationId: string, limit: number): Promise; ``` - API backend: uses bounded conversation message APIs and any required conversation metadata lookup. - Local backend: reads only the active conversation transcript tail from local storage. - Selectors/search may enumerate conversations; normal boot must not. ## Decisions - Tail size should be small and bounded. Start with the existing visual target: enough for recent context, not hundreds of raw messages. - Pending approval is determined by unresolved approval/tool-call state in the tail: approval request exists and no matching tool result/approval response follows it. - The UI should not become `ready` and then do an expensive post-ready transcript replay. - The local fast path must not call APIs that rebuild global message indexes or scan unrelated conversations. - Keep existing hosted/API semantics, but route them through the same tail contract. ## Non-goals - Do not change default resume semantics. - Do not change conversation selectors/search behavior except to keep enumeration out of boot. - Do not rewrite the transcript storage format in this PR. --- ## File: docs/nix.md # Nix and NixOS installation Letta Code ships a flake for Nix users who want a native install path instead of a global npm install. ## Try once From a machine with flakes enabled: ```bash nix run github:letta-ai/letta-code ``` To run from a local checkout: ```bash nix run . ``` ## Install into a profile ```bash nix profile install github:letta-ai/letta-code letta ``` ## Home Manager Add Letta Code as a flake input and enable the module: ```nix { inputs.letta-code.url = "github:letta-ai/letta-code"; outputs = { nixpkgs, home-manager, letta-code, ... }: { homeConfigurations.example = home-manager.lib.homeManagerConfiguration { pkgs = import nixpkgs { system = "x86_64-linux"; }; modules = [ letta-code.homeManagerModules.default { programs.letta-code.enable = true; } ]; }; }; } ``` ## NixOS service For an always-on listener or channel host, enable the NixOS module and provide secrets through a root-readable environment file: ```nix { inputs.letta-code.url = "github:letta-ai/letta-code"; outputs = { nixpkgs, letta-code, ... }: { nixosConfigurations.agent-host = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ letta-code.nixosModules.default { services.letta-code = { enable = true; environmentFile = "/run/secrets/letta-code.env"; extraArgs = [ "listen" ]; }; } ]; }; }; } ``` The environment file can contain values such as: ```dotenv LETTA_API_KEY=... ANTHROPIC_API_KEY=... OPENAI_API_KEY=... SLACK_BOT_TOKEN=... SLACK_SIGNING_SECRET=... ``` Use your normal NixOS secret manager, such as sops-nix or agenix, to materialize that file. ## What this provides The flake exposes: - `packages..default` / `packages..letta-code` - `apps..default` / `apps..letta` - `homeManagerModules.default` and the compatibility alias `homeModules.default` - `nixosModules.default` The package builds the Letta Code CLI from this repository with Bun. Dependency resolution is driven by the checked-in `bun.lock`; the generated `bun.nix` file lets the flake prefetch those Bun dependencies reproducibly for offline Nix builds. The resulting `letta` binary is wrapped with common runtime tools, and agent configuration and credentials stay in the normal Letta Code locations. When `bun.lock` changes, regenerate the Nix dependency expression before opening the PR: ```bash bunx bun2nix -o bun.nix ``` ## Follow-up packaging work This in-repository flake is the fastest path for Nix users to try and deploy Letta Code. A future nixpkgs or Home Manager upstream package should reuse the same shape, but may need additional hardening around native Node dependencies such as `node-pty`, `sharp`, and optional ripgrep binaries. --- ## File: src/agent/prompts/README.md # Prompts All prompt files are imported as text via `promptAssets.ts`. Files use `.md`, `.mdx` (memory blocks with YAML frontmatter), or `.txt` (system reminders injected as XML tags). ## System prompts Selectable via the `/system` command. Each preset is a complete system prompt. Presets that need different standard vs memfs instructions keep separate full prompt files rather than appending memory sections at build time. | File | Used | Description | |------|------|-------------| | `letta_no_memfs.md` | Default for non-memfs agents | Letta-tuned system prompt for standard memory blocks | | `letta.md` | Default for hosted memfs agents | Letta-tuned system prompt for git-backed MemFS memory, including shared memory | | `letta_local_memfs.md` | Default for local backend memfs agents | Letta-tuned system prompt for local-only git-backed MemFS memory | | `source_claude.md` | `/system source-claude` | Near-verbatim Claude Code prompt for benchmarking | | `source_codex.md` | `/system source-codex` | Near-verbatim OpenAI Codex prompt for benchmarking | | `source_gemini.md` | `/system source-gemini` | Near-verbatim Gemini CLI prompt for benchmarking | ### Source prompt provenance #### source_claude.md - **Source:** Claude Code (Anthropic) - **Version:** ~v2.1.50 (Feb 2026) β€” assembled from modular prompt files - **Reference:** https://github.com/Piebald-AI/claude-code-system-prompts - **Notes:** Since v2.1.20 the prompt is composed from ~110 atomic files at runtime. This is the rendered assembly for a default session (no custom output style, standard tools, TodoWrite present, Task subagents available). #### source_codex.md - **Source:** OpenAI Codex CLI (gpt-5.5 model) - **Version:** Extracted from `codex-rs/models-manager/models.json` @ openai/codex `main` (May 2026) - **Reference:** https://github.com/openai/codex - **Notes:** gpt-5.5 uses `model_messages.instructions_template` with a `{{ personality }}` placeholder; this snapshot renders the template substituted with `personality_pragmatic` (the default). Major drift from the prior gpt-5.3-codex snapshot: new senior-engineer framing, expanded engineering judgment guidance, substantially expanded frontend guidance, softer dirty-worktree handling, updated autonomy/compaction instructions, revised formatting/file-link rules, and new anti-creature-language guidance. - **Automation:** `.github/workflows/codex-release-watch.yml` polls stable `openai/codex` releases and files a `codex-watch` issue when upstream tool/schema fields or tool implementation paths change. #### source_gemini.md - **Source:** Gemini CLI (Google) - **Version:** snippets.ts (Feb 2026, copyright 2026 Google LLC) - **Reference:** https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/prompts/snippets.ts - **Notes:** Rendered for interactive mode, git repo present, outside sandbox, standard tools, no sub-agents, no skills, and no YOLO mode. Tool name variables resolved. Conditional sections (YOLO mode, sandbox, GEMINI.md) noted but not inlined. ## Memory blocks (`.mdx`) Default values for agent memory blocks. Loaded via `MEMORY_PROMPTS` in `promptAssets.ts`. Each has YAML frontmatter with `label` and `description`. | File | Used | Description | |------|------|-------------| | `persona.mdx` | Default persona for all new agents | Blank-slate "ready to be shaped" | | `persona_blank.mdx` | Overrides persona for the Blank personality | Prompts user to provide a personality | | `persona_memo.mdx` | Overrides persona for the default Letta Code agent | Warm, curious collaborator personality | | `persona_kawaii.mdx` | Not wired into any agent creation flow | Kawaii voice persona preset | | `human.mdx` | Default human block for all new agents | Placeholder for learning about the user | | `project.mdx` | Registered but not loaded into agents | Placeholder for codebase knowledge | | `style.mdx` | Registered but not loaded into agents | Placeholder for coding preferences | | `memory_filesystem.mdx` | Read-only block for memfs agents | Renders the memory directory tree in-context | ## Skill/command prompts Injected when the user invokes a specific slash command. | File | Used | Description | |------|------|-------------| | `remember.md` | `/remember` command | Instructs the agent to commit conversation context to memory | | `skill_creator_mode.md` | `/skill` command | Guides the agent through designing a new skill | ## System reminders (`.txt`) Short XML-wrapped messages injected into the conversation as system events. | File | Used | Description | |------|------|-------------| | `approval_recovery_alert.txt` | Keep-alive ping | Automated message to resume after approval timeout | | `interrupt_recovery_alert.txt` | User interrupts stream | Notifies the agent the stream was interrupted | --- ## File: src/channels/signal/README.md # Signal channel Letta's first-party Signal channel talks to an external Signal bridge over JSON-RPC + server-sent events. The recommended runtime is native `signal-cli daemon` (`/api/v1/check`, `/api/v1/rpc`, `/api/v1/events`). The configure wizard can also use native `signal-cli` commands for account linking and SMS / voice registration, or use [`signal-cli-rest-api`](https://github.com/bbernhard/signal-cli-rest-api) setup endpoints (`/v1/*`) when that wrapper is available. ## TL;DR Start a local native daemon, configure the account, then run Letta with Signal: ```bash signal-cli -c ~/.local/share/signal-cli-letta daemon \ --http 127.0.0.1:8080 \ --receive-mode on-connection \ --ignore-stories letta channels configure signal letta server --channels signal ``` Use a dedicated Signal number when possible. If you use your personal number, enable `self_chat_mode` and talk to the agent through Signal's Note to Self / self-chat. ## Recommended setup Use a dedicated Signal number for the agent when you want it to participate in ordinary DMs/groups. If you connect Letta to your personal Signal account, enable `self_chat_mode` and talk to it through Signal's Note to Self/self-chat; that mode only permits messages to/from the linked account itself. 1. Start a Signal bridge. Native `signal-cli daemon` is the first-party runtime path: ```bash signal-cli -c ~/.local/share/signal-cli-letta daemon \ --http 127.0.0.1:8080 \ --receive-mode on-connection \ --ignore-stories ``` Alternative: start `signal-cli-rest-api` in JSON-RPC mode. The interactive configure wizard can start this Docker container for you when Docker is available. Docker example: ```yaml services: signal-cli: image: bbernhard/signal-cli-rest-api:latest environment: MODE: json-rpc ports: - "8080:8080" volumes: - signal-cli-data:/home/.local/share/signal-cli ``` 2. Register or link the Signal account in the daemon/config directory. `letta channels configure signal` can help with this after it starts/probes the daemon: - It lists already-linked accounts from `/v1/accounts` when available. - It can open the `/v1/qrcodelink` QR page for device linking. - It can request SMS/voice registration with `/v1/register/{number}` and verify the code with `/v1/register/{number}/verify/{code}`. - If Signal requires captcha, it points you at `signalcaptchas.org` and asks for the returned `signalcaptcha://...` URL. - Native `signal-cli daemon` exposes only Letta's runtime JSON-RPC paths (`/api/v1/*`) and not wrapper setup REST paths (`/v1/*`). In that case the wizard uses local native `signal-cli` commands when available. - Native link renders the emitted `sgnl://linkdevice?...` URI as an ASCII QR in the terminal and parses the final `Associated with: +...` account. - If native link reports the account already exists in the config directory, the wizard offers to use that existing linked account or cancels with the delete-path recovery instruction. Common paths: - QR/device link: use the daemon `/v1/qrcodelink` flow when available, or native `signal-cli link -n "Letta Code"` with the wizard-rendered QR. - SMS registration: register a dedicated number, complete any `signalcaptchas.org` captcha, then verify the SMS code. Upstream references: - - - 3. Run Letta setup: ```bash letta channels configure signal ``` The wizard first checks `http://127.0.0.1:8080`. If no daemon responds and Docker is installed, it can run the container command above automatically using the persistent volume `letta-signal-cli-data`. If a native daemon is already running, it detects the daemon's config directory and can run native setup commands against that directory. 4. Start Letta with Signal enabled: ```bash letta server --channels signal # or local backend: letta server --backend local --channels signal ``` 5. Send the Signal account a DM. With `dm_policy: pairing` (recommended), Letta replies with a pairing code. In the target Letta conversation, run: ```text /channels signal pair ``` ## Manual native `signal-cli` setup If you do not want to rely on the interactive wizard for Signal account setup, you can run native `signal-cli` directly. Use the same config directory for all commands and for the daemon. ```bash export SIGNAL_CLI_CONFIG="$HOME/.local/share/signal-cli-letta" export SIGNAL_ACCOUNT="+15555550100" ``` ### Option A: link an existing Signal account/device Run the link command: ```bash signal-cli -c "$SIGNAL_CLI_CONFIG" link -n "Letta Code" ``` `signal-cli` prints an `sgnl://linkdevice?...` URI. Scan it from Signal mobile: 1. Open Signal on your phone. 2. Go to **Settings β†’ Linked Devices**. 3. Tap **+**. 4. Scan the QR/URI shown by `signal-cli`. When linking succeeds, `signal-cli` prints an associated phone number. Use that number as the Signal account in `letta channels configure signal`. If `signal-cli` says the user already exists in the config directory, you can reuse that account or remove the account directory it reports and link again. ### Option B: register a dedicated Signal number Register the number: ```bash signal-cli -c "$SIGNAL_CLI_CONFIG" -a "$SIGNAL_ACCOUNT" register ``` If Signal requires a captcha, open: ```text https://signalcaptchas.org/registration/generate.html ``` Complete the captcha and copy the returned `signalcaptcha://...` URL, then run: ```bash signal-cli -c "$SIGNAL_CLI_CONFIG" -a "$SIGNAL_ACCOUNT" register \ --captcha 'signalcaptcha://...' ``` After you receive the SMS/voice verification code, verify it: ```bash signal-cli -c "$SIGNAL_CLI_CONFIG" -a "$SIGNAL_ACCOUNT" verify 123456 ``` Registration can de-authenticate other Signal sessions for that phone number, so prefer a dedicated number for bot-style accounts. ### Start the native daemon ```bash signal-cli -c "$SIGNAL_CLI_CONFIG" daemon \ --http 127.0.0.1:8080 \ --receive-mode on-connection \ --ignore-stories ``` Keep the daemon running while Letta is running. ### Smoke-test the daemon Health check: ```bash curl -i --max-time 2 http://127.0.0.1:8080/api/v1/check ``` Event stream check. A timeout is okay as long as headers show HTTP 200 and `text/event-stream`: ```bash curl -i --max-time 2 -N http://127.0.0.1:8080/api/v1/events ``` Optional JSON-RPC send smoke test: ```bash curl -sS http://127.0.0.1:8080/api/v1/rpc \ -H 'content-type: application/json' \ -d '{ "jsonrpc": "2.0", "method": "send", "params": { "account": "+15555550100", "recipient": ["+15555550123"], "message": "Signal daemon smoke test" }, "id": "smoke-test" }' ``` If direct `signal-cli send` works but JSON-RPC `send` fails, restart the daemon. Stale native daemon processes can receive events while JSON-RPC sends fail after account/config changes. Then run: ```bash letta channels configure signal letta server --channels signal ``` ## Config fields Signal accounts live in `~/.letta/channels/signal/accounts.json`. Important fields: | Field | Description | | --- | --- | | `accountId` | Local Letta label for this Signal connection, e.g. `personal` or `bot`. This is not your Signal phone number. | | `base_url` | Signal JSON-RPC/SSE bridge URL, usually `http://127.0.0.1:8080`. | | `account` | Signal account phone number in E.164 format, e.g. `+15555550100`. | | `account_uuid` | Optional advanced loop-protection identity. Only needed when the daemon reports the linked account's own messages as a UUID instead of the configured phone number. | | `agent_id` | Optional agent for account-bound DM/group auto-routing. | | `self_chat_mode` | Personal-number mode. When true, Letta routes only the linked account's own Note to Self/self-chat messages and ignores other DMs on that Signal account. | | `dm_policy` | `pairing`, `allowlist`, or `open`. Pairing is recommended. | | `group_mode` | `disabled`, `mention`, or `open`. Disabled is conservative. | | `allowed_groups` | Optional group ID allowlist when groups are enabled. | | `mention_patterns` | Text aliases/substrings used by mention-mode groups. | | `recipient_aliases` | Optional map from inbound sender identities to replyable recipients, e.g. Signal UUID to E.164 phone number. Useful when native `signal-cli` receives from a UUID but can only send replies to the phone number. | | `download_media` | Download/surface inbound media. Defaults to `true` for new accounts. | | `media_max_bytes` | Maximum inbound media bytes to consider. Default setup value is 25 MiB. | | `transcribe_voice` | Auto-transcribe inbound audio when `OPENAI_API_KEY` is set. | ## Personal-number / self-chat safety model `self_chat_mode` is for using your personal Signal number without letting Letta read or send ordinary Signal DMs from that account. When `self_chat_mode: true`: - Inbound messages route only when they are from the linked account itself, including native `syncMessage.sentMessage` Note to Self events. - Non-self direct messages are dropped before routing. - Outbound sends/reactions are rejected unless the target is the linked account itself. - Groups are not a self-chat target; self-chat mode is intended for Note to Self only. Only one enabled Signal account may use a given `base_url`. Native `signal-cli` event streams are daemon-scoped; if you want multiple Signal accounts enabled at once, run separate `signal-cli daemon` instances with separate config directories and ports, then give each Letta account a distinct `base_url`. When `self_chat_mode: false`: - Messages from the linked account itself are treated as loop-protection echoes and ignored. - DMs/groups follow the normal `dm_policy`, allowlist, pairing, and `group_mode` settings. ## Media and voice notes Inbound images are copied into Letta channel storage and surfaced as both attachment XML and image content parts so agents can inspect them. Other media, including audio, is surfaced with `local_path` metadata in the message envelope. Voice transcription uses the shared OpenAI transcription helper. Some Signal voice notes arrive as raw `.aac`; OpenAI rejects raw AAC uploads, so Letta converts unsupported audio to `.m4a` using `ffmpeg` before upload. If `ffmpeg` is missing, the agent receives an `` in the message envelope telling you to install `ffmpeg` on the listener machine. ## Troubleshooting - **No inbound messages:** confirm the native daemon is listening on `/api/v1/events` or the wrapper is running in JSON-RPC mode, and that Letta's `base_url` points at it. - **Don't know what base URL to use:** run `letta channels configure signal` on the same machine as the listener and let it start/probe the local Docker daemon. Use a custom URL only when the daemon runs elsewhere. - **No account listed by the daemon:** use the configure wizard's QR link flow or SMS/voice registration flow, then rerun account detection. - **QR page says 404:** your daemon exposes runtime JSON-RPC but not `/v1/*` wrapper setup endpoints. Use the native setup path in `letta channels configure signal`; it renders the `signal-cli link` URI as a QR when possible. - **Captcha required:** the wizard opens `signalcaptchas.org`, asks for the returned `signalcaptcha://...` URL, and runs native `signal-cli register --captcha` or the wrapper `/v1/register` equivalent. - **Only placeholders like `[image attached]`:** confirm `download_media: true`, restart the listener after changing config, and check the daemon's attachment directory. - **Voice transcription says unsupported audio or ffmpeg required:** install `ffmpeg` on the machine running `letta server`. - **Pairing repeats:** approve the code in the target Letta conversation with `/channels signal pair ` or use the CLI pairing command. - **Messages from yourself are ignored / bot seems to ignore own linked-device messages:** this is loop protection in normal mode. Enable `self_chat_mode` only when you intentionally want Note to Self/self-chat routing. - **Using your personal Signal number:** enable `self_chat_mode` and talk to the agent in Signal's Note to Self/self-chat. Other direct messages on that linked account are ignored, and outbound sends to non-self targets are rejected while self-chat mode is enabled. - **Agent receives messages but replies fail with internal server error:** check whether the inbound `chat_id` is a UUID like `signal:accd...`. If native `signal-cli` cannot send to that UUID, add a `recipient_aliases` mapping from the UUID to the replyable phone number, then restart the listener. ## Current limitations - Letta does not install or supervise `signal-cli`; you still need a running native daemon or compatible wrapper. - Runtime support targets native `signal-cli` JSON-RPC/SSE. Wrapper `/v1/*` endpoints are used only for setup when available. - Group support is intentionally conservative; start with `group_mode: disabled` or `mention` before using `open`. --- ## File: src/channels/README.md # Channel plugins Letta Code channels connect agents to external chat systems. Telegram, Slack, and Discord are first-party bundled plugins with custom Desktop UI. User-defined plugins are loaded from `~/.letta/channels//` and run headlessly: they can receive inbound messages, participate in pairing/routing, and extend the shared `MessageChannel` tool, but they do not get custom Desktop screens. ## Directory layout ```text ~/.letta/channels/ whatsapp/ channel.json plugin.mjs accounts.json routing.yaml pairing.yaml runtime/ package.json node_modules/ ``` `channel.json` registers the plugin: ```json { "id": "whatsapp", "displayName": "WhatsApp", "entry": "./plugin.mjs", "runtimePackages": ["@whiskeysockets/baileys@6.7.18"], "runtimeModules": ["@whiskeysockets/baileys"] } ``` Rules: - `id` must match the directory name and use lowercase letters, numbers, `_`, or `-`. - `entry` is resolved relative to the channel directory. - `runtimePackages` are installed into `runtime/` by `letta channels install `. - `runtimeModules` are resolved from bundled first-party runtimes first, then from the user channel `runtime/` directory. ## Plugin entry `plugin.mjs` exports either `channelPlugin` or `default`: ```js export const channelPlugin = { metadata: { id: "whatsapp", displayName: "WhatsApp", runtimePackages: ["@whiskeysockets/baileys@6.7.18"], runtimeModules: ["@whiskeysockets/baileys"] }, async createAdapter(account) { return { id: `whatsapp:${account.accountId}`, channelId: "whatsapp", accountId: account.accountId, name: account.displayName ?? "WhatsApp", async start() {}, async stop() {}, isRunning() { return false; }, async sendMessage(message) { return { messageId: crypto.randomUUID() }; }, async sendDirectReply(chatId, text) {}, onMessage: undefined }; }, messageActions: { describeMessageTool() { return { actions: ["send"] }; }, async handleAction({ adapter, request, formatText }) { const formatted = formatText(request.message ?? ""); const result = await adapter.sendMessage({ channel: request.channel, chatId: request.chatId, text: formatted.text, parseMode: formatted.parseMode, threadId: request.threadId }); return `Message sent to ${request.channel} (message_id: ${result.messageId})`; } } }; ``` ## Account model All channels share the same persisted account envelope: ```ts type ChannelAccount = { channel: string; accountId: string; displayName?: string; enabled: boolean; dmPolicy: "pairing" | "allowlist" | "open"; allowedUsers: string[]; config: Record; createdAt: string; updatedAt: string; }; ``` The `config` object is plugin-owned and may contain secrets. In the custom plugin MVP, Desktop/websocket responses do not echo this object back; they only include a generic redacted config summary. First-party bundled plugins can keep custom redaction/compatibility adapters because they also have bespoke Desktop UI. First-party bundled plugins may keep compatibility with their older account fields internally, but user plugins should only rely on `account.config`. ## Runtime behavior The MVP runtime path supports custom plugins that fit the generic pairing and routing flow: 1. The adapter receives an inbound message and calls `adapter.onMessage(msg)`. 2. Letta Code enforces `dmPolicy` / `allowedUsers`. 3. Letta Code resolves a route from `routing.yaml` or creates a pairing code. 4. The routed message is delivered to the bound agent/conversation. 5. ChannelGateway registers `MessageChannel` for the conversation when it has an active route on at least one running channel adapter. Plugins that need Slack/Discord-style auto-routing or rich Desktop management remain first-party/bundled work for now. Custom plugins can still expose custom `MessageChannel` actions and schema fragments via `messageActions`. > Note: inbound channel delivery and user-visible replies are separate steps. > A channel message can successfully reach the agent, but the agent still has to > call `MessageChannel` to reply. If the transcript shows the incoming > `` but no `MessageChannel` tool call, this is usually a > model/prompting issue rather than a channel adapter failure. For debugging, > check whether the notification reached the conversation, whether the agent > called `MessageChannel`, whether the tool result says the message was sent, > and whether the route/account IDs match the original chat. ## Local backend channels Channels can run against the experimental local backend without registering a remote environment. In this mode the backend is in-process, so no `LETTA_BASE_URL` is required. ```bash letta channels install telegram letta channels configure telegram letta server --backend local --channels telegram ``` Then send the bot a message to get a pairing code and bind it to the local agent and conversation: ```bash letta channels pair \ --channel telegram \ --code XXXXXX \ --agent \ --conversation default ``` Only set `LETTA_BASE_URL` for a separate self-hosted server. For example, `LETTA_BASE_URL=http://localhost:8283 letta server --channels telegram` talks to a server running at that URL. Do not set a dummy `LETTA_BASE_URL` for `--backend local`. ## Channel slash commands Typed slash commands are handled before normal channel ingress so operational commands do not get delivered to the agent as regular user messages. The shared channel command set is: - `/status` β€” show account, listener, route, agent, and conversation state. - `/pause` β€” disable agent replies for the current routed chat. - `/resume` β€” re-enable agent replies for the current routed chat. - `/cancel` β€” abort the in-progress agent turn for the current routed chat. - `/chat` β€” show the Letta web chat link for the current route. - `/whoami` β€” show the sender's access scope, tier, and runnable commands. - `/reflection` β€” start a memory reflection pass for the current route's agent conversation when MemFS is enabled. Slack-native slash command payloads currently exist only for `/cancel`; the rest are expected to be sent as normal channel messages in the relevant chat/thread. ## Access control Sender access is decided centrally (`access-control.ts`) on every inbound message β€” DMs and groups, on every channel including auto-routed Slack/Discord traffic β€” before commands or routing run. Per-account fields (in `accounts.json`, snake_case): - `dm_policy` β€” `"open"`, `"allowlist"`, or `"pairing"` for direct messages. Slack note: `"pairing"` is a legacy unenforced default on Slack and behaves as `"open"`; use `"allowlist"` to restrict Slack DMs. - `allowed_users` β€” sender IDs allowed regardless of policy. `"*"` allows everyone. WhatsApp/Signal entries match through identity normalization (phone digits / UUID vs E.164). - `group_policy` β€” `"open"` (default, historical behavior) or `"allowlist"`, which restricts group/channel senders to the allowlists below plus paired users. - `admin_users` β€” enables slash-command tiers when non-empty: admins run everything; other users get the read-only floor (`/help`, `/status`, `/whoami`) plus `user_allowed_commands`. When unset, every allowed user has full command access. - `user_allowed_commands` β€” extra commands non-admins may run. Env vars (comma-separated user IDs; merged with account config): - `LETTA_CHANNELS_ALLOWED_USERS` / `LETTA__ALLOWED_USERS` β€” global / per-channel allowlists. Once configured, they restrict all scopes (DMs and groups) on the affected channels. - `LETTA_CHANNELS_ADMIN_USERS` / `LETTA__ADMIN_USERS` β€” admin tiers. - `LETTA_CHANNELS_ALLOW_ALL_USERS=1` / `LETTA__ALLOW_ALL_USERS=1` β€” explicit opt-out of sender gating. Pairing approvals are a union with the allowlists: a paired sender stays allowed in any scope, and allowlisted senders skip the pairing handshake. ## Slack app manifest notes The bundled Slack channel runs in Socket Mode. The Slack app must still declare the events, scopes, and slash commands that Slack should deliver to Letta Code. For `/cancel`, add the `commands` bot scope and a native slash command entry: ```yaml features: slash_commands: - command: /cancel url: https://example.com/slack/commands usage_hint: "" should_escape: false oauth_config: scopes: bot: - app_mentions:read - channels:history - chat:write - commands - files:read - files:write - groups:history - im:history - reactions:read - reactions:write - users:read settings: event_subscriptions: bot_events: - app_mention - message.channels - message.groups - message.im - reaction_added - reaction_removed socket_mode_enabled: true ``` Slack-native slash command payloads do not identify a thread. If `/cancel` is sent through Slack's native command UI in a channel, Letta Code can target the sole routed thread in that channel; if multiple Letta threads are routed there, send `/cancel` as a normal thread message instead so the thread route is unambiguous. The slash command `url` is present because Slack manifests require one; the Socket Mode listener receives the command over the app-level WebSocket. ### Per-channel mention-only Slack bots By default, once a Slack bot participates in a thread, subsequent human replies in that thread route to the agent without another mention. To require an explicit `@mention` in selected channels, add their Slack channel IDs to the account's `mention_only_channels` list in `~/.letta/channels/slack/accounts.json`: ```json { "mention_only_channels": ["C0123456789"] } ``` This policy is account-specific and applies even to threads where that bot has already participated. Add the channel ID to every bot account that should require an explicit mention. In listed channels, unmentioned messages and reaction events do not start agent turns. Direct messages, native Slack slash commands, and channels not listed here retain their existing behavior. Keep both `app_mention` and the broad `message.channels` / `message.groups` subscriptions when only selected channels should be mention-only. Removing the broad message events makes mention-only delivery apply to the whole Slack app instead. Restart the listener after editing `accounts.json` so the running Slack adapter loads the new policy. ## First-party vs user plugins First-party plugins are bundled in `src/channels//` and registered by the built-in registry. They can have bespoke Desktop UI and compatibility shims. User plugins are discovered from `~/.letta/channels//channel.json`. They are intentionally headless in this MVP. They should be configured by editing `accounts.json` or by sending generic websocket/CLI account updates whose plugin-owned fields live under `config` / `plugin_config`. --- ## File: src/cli/app/README.md # CLI App Layout `src/cli/App.tsx` is the public entrypoint. This directory holds the actual implementation, split by responsibility so agents can open the smallest useful file first. - `App.tsx`: local entrypoint that re-exports the coordinator. - `AppCoordinator.tsx`: Ink state, effects, overlay wiring, and render tree. - `AppView.tsx`: render-only Ink tree for the coordinator. - `useSubmitHandler.ts`: slash-command and user-submit router. - `useConversationLoop.ts`: streaming turn loop, retry/recovery, tool execution, and reflection auto-launch. - `useApprovalFlow.ts`: approval recovery and approve/deny batching. - `useConversationSwitching.ts`: `/btw`, agent selection, and new-agent conversation switching flows. - `useBashHandlers.ts`: bash-mode submit/interrupt handling. - `useQueuedApprovalSubmit.ts`: stale approval recovery helpers used before slash-command sends. - `useFeedbackHandler.ts`: `/feedback` submission and diagnostics payload. - `useInterruptHandler.ts`: ESC interrupt, cancellation, and recovery cleanup. - `useReasoningCycle.ts`: reasoning-tier tab cycling and debounced persistence. - `useConfigurationHandlers.ts`: model/system/personality/toolset/experiment selectors and their queued overlay actions. - `constants.ts`: timing, retry, provider fallback, and layout constants. - `modelConfig.ts`: model handle, reasoning effort, and model error hint helpers. - `commandRouting.ts`: slash command queue-bypass classification. - `approvalDiffs.ts`, `approvalQuestions.ts`: approval helpers. - `ids.ts`, `layout.ts`, `contentParts.ts`, `systemReminders.ts`: transcript and content utilities. - `reflection.ts`, `retry.ts`, `session.ts`, `notifications.ts`, `errors.ts`: focused runtime helpers. - `StaticTranscript.tsx`, `ExitStats.tsx`: render-only pieces extracted from the coordinator. - `types.ts`: app props and static transcript item types. The split intentionally mirrors the listener layout at a coarse level: `useConversationLoop.ts` is the interactive analogue of listener `turn.ts`, while `useSubmitHandler.ts` owns the command-routing surface that can converge with listener command handling in a later PR. --- ## File: src/tools/README.md # Client-side tool guidelines How to implement tools that run locally in Letta Code. ## Contract - Function signature: `(args, opts?) => Promise<{ toolReturn: string; status: "success" | "error"; stdout?: string[]; stderr?: string[] }>` - Optional `opts.signal?: AbortSignal`. If you spawn a subprocess, wire this signal to it so Esc/abort can kill it cleanly. If you’re pure in-process, you can ignore it. ## Subprocess tools (e.g., Bash) - Pass the provided `AbortSignal` to `exec`/`spawn` so abort kills the child. Normalize abort errors to `toolReturn: "User interrupted tool execution", status: "error"`. - Avoid running multiple subprocesses unless you also expose a cancel hook; we execute tools serially to avoid races. ## In-process tools (read/write/edit) - You can ignore the signal, but still return a clear `toolReturn` and `status`. - Be deterministic and side-effect aware; the runner keeps tools serial to avoid file races. ## Errors - Return a concise error message in `toolReturn` and set `status: "error"`. - Don’t `console.error` from tools; the UI surfaces the returned message.