#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
A production user (51 blocks, 115k tokens) hit compaction loops because
the system prompt alone exceeded the context window. Root cause: no
limits on block count or total memory size.
Changes to bundled agent:
- Hard cap: 12 blocks max, 30k chars total across all blocks
- Per-block limits reduced: 5k for system blocks, 3k for user blocks
(down from 20k each — 160k theoretical max was dangerous)
- self_improvement rewritten: consolidate-first policy, aggressive
trimming, explicit "every line must earn its place"
- core_directives: memory limit reminder near the top
Worst case with these limits: ~10.5k tokens of system prompt.
Previously: unbounded (observed 115k tokens in production).
Written by Cameron ◯ Letta Code
"Constraints inspire creativity." - Biz Stone
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>
- Guidance block is now general-purpose, not session-specific. Do NOT
clear on session start — multiple CC sessions share the same block.
- Call out Task tool / sub-agent spawning in full mode
- Note that glm-5 is free but users can pick any model via the ADE
Written by Cameron ◯ Letta Code
"The whole is greater than the sum of its parts." - Aristotle
- 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
The top-level framing now reflects that Sub is a full background agent
with client-side tool access (Read, Grep, Glob, web search), not just
a passive memory layer. Updated architecture diagram, use cases, agent
description, and API notes.
Written by Cameron ◯ Letta Code
"If you can't explain it simply, you don't understand it well enough." - Albert Einstein
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
Add Read, Glob, Grep to tool_guidelines so Sub knows it has
filesystem access via the Letta Code SDK transport.
Written by Cameron ◯ Letta Code
"Know thyself." - Socrates
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>