#47: Skip /dev/tty entirely on Windows — it resolves to C:\dev\tty
which doesn't exist. The .on('error') handler from v2.1.1 catches the
async error on Linux, but on Windows we shouldn't even attempt it.
#48: Expand common shell syntax ($HOME, ${HOME}, ~) in LETTA_HOME.
When set via Claude Code settings.json, env vars aren't shell-expanded,
so "$HOME" becomes a literal directory name. expandPath() now resolves
these to os.homedir(). Fixes both getDurableStateDir() call sites and
the splash screen display.
Fixes#47, fixes#48.
Written by Cameron ◯ Letta Code
"Defensive programming is the art of expecting the unexpected." - Unknown
#42: spawnSilentWorker on macOS/Linux now uses the plugin's local tsx
CLI (node_modules/tsx/dist/cli.mjs) instead of npx, which resolves to
a global cache that can't find @letta-ai/letta-code-sdk. Same pattern
Windows already used.
#41: Add error handler on /dev/tty WriteStream in session_start.ts.
createWriteStream returns synchronously but ENXIO fires async — without
a handler, Node crashes the process.
#34: Partial workaround for CLAUDE_PLUGIN_ROOT being empty on Linux.
hooks.json now uses ${CLAUDE_PLUGIN_ROOT:-.} fallback. silent-npx.cjs
re-resolves broken script paths via __dirname when the original path
doesn't exist. Note: this is primarily a Claude Code framework bug.
Fixes#42, fixes#41, partially addresses #34.
Written by Cameron ◯ Letta Code
"First, do no harm." - Hippocrates
Ensure imported and existing Subconscious agents always include origin:claude-subconcious for tracking, and keep git-memory-enabled tagging in the same idempotent path.
👾 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta Code <noreply@letta.com>
- Enable memfs on import: adds `git-memory-enabled` tag after importing
the bundled agent, which triggers the server to create a git-backed
memory repo and sync blocks as files
- Update system prompt: "memory blocks" → "files in a git-backed
filesystem (memfs)"
- Update self_improvement: reference memfs conventions, file management
instead of raw block management
- Update core_directives: "memory files" instead of "memory blocks"
Existing users' agents will get memfs when Letta Code enables it on
their cloud agent. New users get it automatically on import.
Closes#32
Written by Cameron ◯ Letta Code
"Simplicity is the ultimate sophistication." - Leonardo da Vinci
Use a shared URL builder across scripts and update createConversation to call /v1/conversations/ with query params to avoid HTTPS->HTTP redirect failures behind reverse proxies. This also deduplicates LETTA_API_BASE handling and adds regression tests for trailing-slash behavior.
👾 Generated with [Letta Code](https://letta.com)
Co-authored-by: Letta Code <noreply@letta.com>
- Session start message to Sub now includes sdk_tools_mode and
describes what tools are available (read-only, full, or off)
- letta_context header to Claude describes Sub's capabilities
("can read files, search your codebase" vs "listen-only mode")
- Updated core_directives in bundled agent to reflect tool access
- Updated system prompt and description to "Subconscious" framing
Written by Cameron ◯ Letta Code
"Know thyself." - Socrates
off now means "no client-side tools" — Sub still receives transcripts
via the SDK transport but can only use memory operations. Useful for
listen-only agents that observe without filesystem access.
Written by Cameron ◯ Letta Code
"Listening is an art that requires attention over talent." - Dean Jackson
- Remove checkpoint docs, legacy `off` mode, and stale log file refs from README
- Clean up verbose per-message stream logging in SDK worker (keep tool calls + errors)
- Bump version to 2.0.0 in package.json and plugin.json
Written by Cameron ◯ Letta Code
"Less is more." - Ludwig Mies van der Rohe
BREAKING CHANGE: The `LETTA_SDK_TOOLS=off` option is removed. All
message delivery to Sub now goes through the Letta Code SDK. Users
who need the old behavior should pin to a pre-SDK tag.
Deleted:
- scripts/send_worker.ts (legacy raw API worker)
- scripts/plan_checkpoint.ts (was no-op in SDK mode)
Removed:
- Legacy branch in send_messages_to_letta.ts
- Early prompt notification in sync_letta_memory.ts
- sendMessageToConversation from conversation_utils.ts
- Checkpoint hook from hooks.json
Written by Cameron ◯ 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-Exupéry
In SDK mode, sync_letta_memory.ts and plan_checkpoint.ts were still
spawning legacy send_worker.ts — racing the SDK worker on the same
conversation and causing 409 CONFLICT errors (0 chars response).
- sync_letta_memory: skip early prompt notification in SDK mode
- plan_checkpoint: skip entirely in SDK mode (Stop hook handles it)
Written by Cameron ◯ Letta Code
"Two threads enter, one thread leaves." - Mad Max, concurrent edition
Revert to resumeSession(conversationId) and log every stream message
type to diagnose why the session returns 0 chars.
Written by Cameron ◯ Letta Code
"Debugging is twice as hard as writing the code." - Brian Kernighan
resumeSession(conversationId) silently fails (0 chars response) when the
conversation was created via the raw Letta API. The SDK needs to manage
its own conversation. Revert to agentId for now.
Written by Cameron ◯ Letta Code
"Move fast and fix things." - Unknown
resumeSession(conversationId) instead of resumeSession(agentId) so that
SDK-routed messages show up in the same conversation on app.letta.com.
Written by Cameron ◯ Letta Code
"All problems in computer science can be solved by another level of indirection." - David Wheeler
Give the Subconscious agent client-side tool access (Read, Grep, Glob,
web_search) via the Letta Code SDK. Instead of being limited to memory
operations, Sub can now read files and search the web while processing
transcripts.
Architecture:
- New send_worker_sdk.ts uses resumeSession() from @letta-ai/letta-code-sdk
- send_messages_to_letta.ts routes to SDK or legacy worker based on
LETTA_SDK_TOOLS env var (read-only | full | off)
- Stop hook is now async (won't block Claude Code)
- Legacy raw API path preserved for LETTA_SDK_TOOLS=off
New env var: LETTA_SDK_TOOLS
- read-only (default): Read, Grep, Glob, web_search, fetch_webpage
- full: all tools
- off: legacy memory-only behavior
Closes#19
Written by Cameron ◯ Letta Code
"The best interface is no interface." - Golden Krishna
- Simplified spider mascot with red eyes
- Print Discord/agent links before blocking network call
- Ensures links display even if session message times out
- Add PreToolUse hooks for AskUserQuestion and ExitPlanMode to send
transcripts to Letta at natural pause points (plan_checkpoint.ts)
- Extract shared transcript utilities into transcript_utils.ts
- Add LETTA_CHECKPOINT_MODE env var (blocking/async/off)
- Add startup splash screen with agent info, settings, and links
- Write to /dev/tty to show splash in terminal (bypasses Claude capture)
- Update README with checkpoint hooks documentation
The hardcoded /tmp/letta-claude-sync/ path caused EACCES errors when
multiple OS users shared the same machine — the first user to create
the directory owned it, blocking all others.
Now uses os.tmpdir() with a UID suffix (e.g. /tmp/letta-claude-sync-501/)
so each user gets their own log/payload directory.
Fixes#25
Written by Cameron ◯ Letta Code
"Sharing is caring, except for /tmp directories." - Unknown
The top-level `{ model: "..." }` PATCH on the Letta API resets
context_window to a server-side default (32K), even when both fields
are sent in the same request. This means every LETTA_MODEL override
silently drops the agent's context_window back to 32K.
Switch updateAgentModel() to use `{ llm_config: {...} }` PATCH format
which preserves context_window and other settings. Add buildLlmConfig()
helper that constructs the full config from available model metadata and
the agent's current settings.
Also adds LETTA_CONTEXT_WINDOW environment variable so users can
explicitly set their desired context window size.
Changes:
- Extract findModel() from isModelAvailable() for reuse
- Add LlmConfig interface with full llm_config fields
- Add buildLlmConfig() to construct config from model handle + metadata
- Update updateAgentModel() to PATCH via llm_config instead of model
- Add LETTA_CONTEXT_WINDOW env var support
- Document LETTA_CONTEXT_WINDOW in README
- Add 13 tests for findModel() and buildLlmConfig()
- Add dual P/Invoke for UpdateProcThreadAttribute (Win11 26300+) vs
UpdateProcThreadAttributeList (Win10/older) with EntryPointNotFoundException
fallback in SilentLauncher.cs
- Extract shared spawnSilentWorker() utility into conversation_utils.ts,
replacing ~50 duplicated lines in send_messages_to_letta.ts and
sync_letta_memory.ts
- Add build.ps1 for reproducible silent-launcher.exe builds
- Rebuild silent-launcher.exe with updated source
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On Windows 11 / Windows Terminal, hook execution caused visible console
window popups. This replaces the simple npx wrapper (silent-npx.js) with
a PseudoConsole (ConPTY) + CREATE_NO_WINDOW approach that runs scripts
in a headless console session.
New files:
- SilentLauncher.cs: C# launcher creating a PseudoConsole with
CREATE_NO_WINDOW, stdin/stdout via temp files, and --import tsx/esm
for single-process execution
- silent-launcher.exe: Compiled as AnyCPU winexe (works on x64 and ARM64)
- stdio-preload.cjs: Node.js --require script for temp file I/O
- silent-npx.cjs: Cross-platform shim that delegates to
silent-launcher.exe on Windows, runs tsx directly elsewhere
Also fixes:
- Letta API message sync: bumped limit from 50 to 300 and added date
sorting (API does not guarantee newest-first ordering)
- Background workers on Windows: spawn through silent-launcher.exe with
detached:true so workers survive PseudoConsole closure
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The cleanLettaFromClaudeMd() function only targeted the project-level
.claude/CLAUDE.md. Users with pre-v1.3.0 installations could have
bloated <letta> sections in their global ~/.claude/CLAUDE.md that
were never cleaned up, causing Claude Code to warn about large files.
Now session_start also cleans the global CLAUDE.md, with a guard to
avoid double-cleaning if cwd happens to be the home directory.
Fixes#16
Written by Cameron ◯ Letta Code
"The details are not the details. They make the design." - Charles Eames
Co-authored-by: Cameron <cpfiffer@users.noreply.github.com>
- whisper (default): Only inject Sub's messages
- full: Full memory blocks on first prompt, diffs + messages after
- off: Disable all hooks
No mode writes to CLAUDE.md.
Written by Cameron ◯ Letta Code
"Know the difference between detail and clutter." - Unknown
Subconscious never writes to CLAUDE.md in any mode. All memory is
injected via stdout into prompt context. Two modes:
- whisper (default): Full blocks on first prompt, diffs + messages after
- off: Disable all hooks
Existing <letta> sections in CLAUDE.md are cleaned up automatically.
If CLAUDE.md was entirely created by us, it gets deleted.
If user had their own content, only the <letta> section is stripped.
Written by Cameron ◯ Letta Code
"Less is more." - Mies van der Rohe
- LETTA_HOME: Base directory for .letta/ state files (conversations, sessions)
- LETTA_PROJECT: Base directory for .claude/CLAUDE.md memory blocks
Both default to current behavior but can be set to $HOME to consolidate
all plugin output in one location, preventing file proliferation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Allows users to override the default CLAUDE.md location to prevent
file proliferation when the plugin is globally enabled.
When set (e.g., to $HOME/.claude/CLAUDE.md), the plugin writes to
that single location regardless of the current working directory.
This solves the duplication issue where Claude Code's recursive
discovery finds multiple CLAUDE.md files in parent and child
directories, injecting duplicate memory blocks into context.
Changes:
- scripts/conversation_utils.ts: Check LETTA_CLAUDE_MD_PATH env var
- README.md: Document the new optional environment variable
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
All files converted to Unix line endings (LF) for consistent
cross-platform development in WSL environment.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Claude Sonnet 4.5 first (recommended for agents by Anthropic)
- GPT-4.1-mini second (good balance, cheap)
- Claude Haiku 4.5 third (fast Claude option)
- Flagship models as fallback only
- Updated model examples in README to current versions
Resolve conflicts from divergent histories:
- Keep LETTA_MODEL env var and auto-detection features from this PR
- Merge agent ID validation from main
- Update to version 1.1.0
- Include Windows compatibility fixes (NPX_CMD)
- Restore pretool_sync.ts, tests, and CHANGELOG
Written by Cameron ◯ Letta Code
"Simple things should be simple, complex things should be possible." - Alan Kay
Co-authored-by: Cameron <cpfiffer@users.noreply.github.com>
On Windows, spawning detached child processes with `spawn()` requires
`shell: true` to work properly, otherwise Node.js throws EINVAL error.
This adds the `shell: true` and `windowsHide: true` options when running
on Windows platform to fix the "spawn EINVAL" error reported in issue #8.
Fixes#8
Written by Cameron ◯ Letta Code
"The only way to make sense out of change is to plunge into it, move with it, and join the dance." - Alan Watts
Co-authored-by: Cameron <cpfiffer@users.noreply.github.com>
Same as PreToolUse - inject instruction dynamically so Claude
surfaces Sub messages to the user. Not in CLAUDE.md which gets
forgotten as context grows.
Written by Cameron ◯ Letta Code
"Context is everything." - Unknown
Add instruction in additionalContext telling Claude to briefly
acknowledge what Subconscious said, so users see it inline.
Written by Cameron ◯ Letta Code
"Show, don't tell." - Chekhov
Add systemMessage to PreToolUse output so users can see when Sub has
something to say mid-workflow. Shows a preview of the message with 💭.
Written by Cameron ◯ Letta Code
"Make the invisible visible." - Edward Tufte
New lightweight hook that checks for Letta agent updates before each
tool use, addressing "workflow drift" in long workflows. Injects new
messages and memory block diffs via additionalContext if changes found,
silent no-op otherwise.
Written by Cameron ◯ Letta Code
"The only way to do great work is to love what you do." - Steve Jobs
SessionStart now writes CLAUDE.md immediately after creating or
resuming a conversation. This ensures Claude sees current IDs from
the start rather than stale data from a previous session.
- Extract shared sync functions to conversation_utils.ts
- Add IS_HOSTED flag: show app URLs for hosted, raw IDs for self-hosted
- Remove duplicate code from sync_letta_memory.ts
Adds comprehensive test suite using vitest:
- Valid agent ID formats (lowercase, uppercase, mixed case)
- Invalid friendly names (e.g., "Memo", "My Agent")
- Missing "agent-" prefix
- Malformed UUIDs (truncated, extra chars, wrong segments)
- Edge cases (empty string, whitespace, newlines)
Co-authored-by: Cameron <cpfiffer@users.noreply.github.com>
Written by Cameron ◯ Letta Code
"A program that has not been tested does not work." - Bjarne Stroustrup
Instead of requiring users to manually set LETTA_MODEL, the plugin now:
1. Queries available models from the Letta server
2. Checks if the agent's current model is available
3. Auto-selects a fallback if not (preferring openai/gpt-4o-mini)
4. Informs user via console output about model changes
LETTA_MODEL can still be used for manual override.
Fixes#2
Written by Cameron ◯ Letta Code
"Make it work, make it right, make it fast." - Kent Beck
Users were getting confusing SQL foreign key constraint errors when using
a friendly agent name (e.g., "Memo") instead of the proper UUID format.
This adds validation that:
- Checks the agent ID matches the expected format (agent-UUID)
- Provides a clear error message explaining the correct format
- Lists common mistakes like using the friendly name
- Includes instructions on how to find the correct agent ID
For saved configs with invalid IDs, we gracefully fall back to importing
the default agent instead of crashing.
Fixes#5
Co-authored-by: Cameron <cpfiffer@users.noreply.github.com>
Written by Cameron ◯ Letta Code
"The details are not the details. They make the design." - Charles Eames
When using self-hosted Letta with providers other than z.ai, users can now
set LETTA_MODEL to override the default model (e.g., "openai/gpt-4o-mini").
The model is applied:
- After auto-importing the default agent
- When the saved config has a different model
- When using LETTA_AGENT_ID env var
Fixes#2
Written by Cameron ◯ Letta Code
"Simplicity is the ultimate sophistication." - Leonardo da Vinci
Use npx.cmd instead of npx on Windows platforms to fix spawn ENOENT errors.
Fixes#1
Written by Cameron ◯ Letta Code
"The only way to do great work is to love what you do." - Steve Jobs
Co-authored-by: Cameron <cpfiffer@users.noreply.github.com>
- Fix message ordering: API returns newest-first, sync was iterating
wrong direction. Now iterates from 0 to lastSeenIndex for new messages.
- Add lookupConversation() to recover conversationId from conversations.json
when session state is missing it (handles state corruption/reset).
- Extract shared utilities to conversation_utils.ts
- Add LETTA_DEBUG=1 env var for debug logging to stderr
- Add matcher: "*" to hooks for Claude Code compatibility
- Use conversation URL (with ?conversation=id) as primary link
- Update first message instruction to point users directly to their conversation
Written by Cameron ◯ Letta Code
"The best way to predict the future is to invent it." - Alan Kay
Change fetchLastAssistantMessage to use conversation-specific endpoint
(/conversations/{id}/messages) instead of agent-wide endpoint
(/agents/{id}/messages). Prevents mixing messages from multiple
conversations and ensures we fetch responses from the correct session.