Skip to content

letscode-memory

Cross-session memory for letscode, shipped as the canonical reference plugin. Drop markdown files in ~/.letscode/memory/; the plugin injects them as background context at the top of every LLM turn.

The plugin lives at plugins/letscode-memory/ in the main repo — outside src/letscode/, in its own package with its own pyproject.toml, validating that the extension model works for a real third-party-shaped plugin.

Install

Install the plugin into the same environment as letscode:

uv tool install letscode --with letscode-memory   # once published
# or, from a locally built wheel:
uv pip install --python <letscode-env> letscode_memory-*.whl
uv pip install -e plugins/letscode-memory/

Either way, letscode discovers the plugin via its letscode entry point — no further setup. Startup loads /remember, /memories, and /forget and activates the memory transform. Discovery from a clean, non-editable install is gated in CI-style by make verify-plugin.

Use

> /remember Likes terse responses, no preamble.
> /remember Prefers Python over JavaScript when both work.
> /memories

Memories are plain markdown files in ~/.letscode/memory/. Filename is the UTC ISO timestamp of creation (2026-05-13T103045Z.md); the file's body is the memory itself. Hand-edit freely — the plugin reads them fresh each turn.

Recall budget

Up to the 20 most recent memories are injected per turn. Beyond that the prompt cost is no longer worth the recall value — either prune (rm ~/.letscode/memory/<id>.md) or move long-form context into a skill.

How it hooks in

Three hooks:

  • letscode_register_commands — adds /remember and /memories.
  • letscode_transform_context — prepends a CustomMessage(kind="memory") to the turn's message list. Returns None (opt out) when there are no memories.
  • letscode_render_custom_message — renders the memory marker into a UserMessage at LLM-call time. Returns None for any other kind, so other plugins' renderers (including the built-in's compaction handler) keep working.

That's the entire surface. No tools, no skills, no frontend.

This is the composable pattern: the CustomMessage lives in the persisted transcript (so it survives session save/load), and the per-kind render_custom_message hook lets the memory plugin and the built-in compaction plugin coexist without shadowing each other.

Why a CustomMessage and not a UserMessage?

A UserMessage prepended to the message list would also work — but it would also be persisted as a "user said this" entry, which is confusing when the user is actually about to type something else. The CustomMessage lives in the transcript with kind="memory" and is rendered into a UserMessage-shaped LLMMessage just for the model. The transcript stays clean.

Earlier in v0.3 the plugin used a UserMessage directly because the letscode_convert_to_llm hook didn't compose across plugins. That gap was resolved later in v0.3 by introducing letscode_render_custom_message (per-CustomMessage-kind dispatch, firstresult=True). The plugin migrated to the cleaner path; see the extension model doc §5.11 for the design rationale.

Source

The full source is in plugins/letscode-memory/:

plugins/letscode-memory/
├── pyproject.toml          # entry point + dev-only [tool.uv.sources]
├── README.md
├── src/letscode_memory/
│   ├── __init__.py
│   ├── plugin.py           # @hookimpl declarations (~120 lines)
│   └── store.py            # filesystem-backed memory list (~80 lines)
└── tests/test_plugin.py    # 20 tests, imports only from stable surface

Total: ~200 lines of source plus tests. The shape any external plugin author can copy.