Memory V1: remembered facts, your instructions, one screen

Coworkers remember durable things you tell them and use them in future sessions.
One Settings screen lists everything remembered - edit, delete, or stop new saves; standing instructions ride along.
Knowledge is session-stable, the save switch is per-message; sqlite gains a summary column via in-place migration.
This commit is contained in:
Devika Verma
2026-07-28 20:32:14 +05:30
parent 3766805d10
commit ef59b0f39a
19 changed files with 1717 additions and 47 deletions
+100 -11
View File
@@ -23,7 +23,13 @@ from .connectors import (
)
from .engine import Approver, TurnEngine
from .environment import environment_context
from .memory import MemoryStore, Scope, format_memories, memory_tools
from .memory import (
MemoryStore,
Scope,
format_user_rules,
memory_tools,
render_memory_block,
)
from .permissions import Mode, PermissionEngine
from .project import load_agents_md
from .roots import RootDir, normalize_roots, render_context
@@ -57,21 +63,50 @@ in which files, how you'll verify) — don't describe edits as if you were makin
the plan is approved, this same session switches to execution and you implement it; if
rejected, revise the plan using the feedback."""
# When-to-remember rules, injected only when a memory store is wired. Without these,
# models either never call `remember` or save noise the repo already records.
# When-to-remember rules (MEMORY-SPEC §4.2), injected only when a memory store is wired.
# Without these, models either never call `remember` or save noise the repo already
# records. The conservative bias is deliberate: a wrong memory feels broken and creepy at
# once; a missing one merely means the user repeats themselves.
_MEMORY_GUIDANCE = """\
Memory:
- You have persistent memory across sessions. Use `remember` for durable facts: the user's \
corrections and stated preferences (include the why), and project context you couldn't \
rederive from the code. Don't save what the repo already records (code structure, git \
history, AGENTS.md) or details that only matter to the current task. Use absolute dates, \
never "yesterday".
rederive from the code. Scope by what the fact is about: facts about the user -> "global"; \
facts about the current work -> "workspace". Always pass a one-line summary (15 words max) \
alongside the full content.
- Save conservatively — a wrong memory costs more than a missing one. Save only clearly \
durable facts ("from now on", "always", "in all my chats"). Ambiguous one-off phrasing \
("I prefer simple talking"): apply it now, don't save it. But when the user explicitly \
asks you to remember something, always save it.
- Sensitive topics (health, finances, relationships, beliefs): never save silently. Ask \
first — "Want me to remember this for next time?" — and save only on a yes.
- When you save, say so in one short plain sentence in your visible reply ("I'll remember \
that you prefer short replies."). And the first time a remembered fact shapes your \
behavior in a session, note it in one quiet line ("Keeping this short since you prefer \
simple replies.") — first use only, not every message.
- Don't save what the repo already records (code structure, git history, AGENTS.md) or \
details that only matter to the current task. Use absolute dates, never "yesterday".
- Before saving, check the known-memories list: if an entry already covers it, revise that \
entry with `memory_update` instead of adding a near-duplicate; retire wrong or obsolete \
entries with `memory_forget`.
- Memories reflect when they were written. If one names a file, flag, or URL, verify it \
still exists before relying on it."""
# Injected INSTEAD of the memory guidance when the user turned memory off (§4.3).
# Off means "stop LEARNING", not "forget what you know": already-saved memories stay
# injected and usable; only the write tools are gone. Without this notice the model
# bluffs — asked to "remember" with no remember tool, it narrated a fake save through
# its todo list ("I'll remember that your favorite color is blue"), observed live
# 2026-07-28. Honesty needs the model to KNOW saving is off, not just lack the tools.
_MEMORY_OFF_NOTICE = """\
Saving new memories is turned off in this user's Settings. What you already know about \
them (the known-memories list, if any) is still true and you should keep using it — but \
you have no way to save, change, or delete anything, and nothing new from this \
conversation will carry over to future ones. If the user asks you to remember something \
new, state both halves plainly: you'll keep it in mind for the rest of this conversation, \
but it won't be saved once the conversation ends — they can turn saving back on in \
Settings ▸ Memory. Never imply you saved, noted, or will remember anything new."""
# UX-015 (§33): the GUI interleaves these status lines with humanized tool rows inside a
# collapsed "turn" — they're what the user reads while the agent works. Universal (appended
# for every persona); models that ignore it degrade gracefully to a turn with no narration.
@@ -118,6 +153,21 @@ def build_engine(
max_iterations: Optional[int] = None,
model_settings: Optional[dict[str, Any]] = None,
memory_store: Optional[MemoryStore] = None,
# MEMORY-SPEC §5.1: called with the MemoryItem right after `remember` persists it —
# the manager uses this to push the memory_saved event that powers the save toast.
on_memory_saved: Optional[Any] = None,
# MEMORY-SPEC §6: the user's standing rules (Settings textarea). Injected verbatim
# above auto memories; independent of the memory on/off switch. No tool writes it.
# A CALLABLE is read per turn (the server passes one so a Settings edit reaches
# conversations already open); a plain string is a fixed value for CLI/tests.
user_rules: Optional[Any] = None,
# True when the user turned memory OFF in Settings (vs. memory simply not wired):
# injects the honesty notice so the model says so instead of faking a save.
memory_off: bool = False,
# LIVE saving switch, consulted per write so turning memory off applies to
# conversations already running (the registry is fixed at build, so the tool stays
# and refuses). Same pattern as the skills menu's live filter.
memory_saving_enabled: Optional[Any] = None,
messages: Optional[list[dict[str, Any]]] = None,
extra_tools: Optional[list[Any]] = None,
secrets: Optional[SecretStore] = None,
@@ -247,15 +297,46 @@ def build_engine(
if conventions:
instructions = f"{instructions}\n\n{conventions}"
# The user's own standing instructions, read once here: like the memories below,
# they're session-stable knowledge. Edits apply to NEW conversations (the Settings
# copy says exactly that), never mid-conversation.
rules_block = format_user_rules(
(user_rules() if callable(user_rules) else user_rules) or ""
)
if rules_block:
instructions = f"{instructions}\n\n{rules_block}"
# The live saving switch. The callable (server) beats the build-time flag (CLI/tests):
# the setting can flip EITHER WAY mid-conversation, so nothing about it may be baked
# into the fixed registry or the static instructions (owner-hit 2026-07-28, both
# directions: off kept saving, then on kept claiming it was off).
def _saving_enabled() -> bool:
if memory_saving_enabled is not None:
return bool(memory_saving_enabled())
return not memory_off
if memory_store is not None:
# Always the full toolset: the registry is fixed at build, so a session born
# while saving was off must still be able to save the moment it's turned on.
# Enforcement is the tools' own live check, not their absence.
registry.register_all(
memory_tools(memory_store, workspace=str(ws) if ws else None)
memory_tools(
memory_store,
workspace=str(ws) if ws else None,
on_saved=on_memory_saved,
saving_enabled=_saving_enabled,
)
)
instructions = f"{instructions}\n\n{_MEMORY_GUIDANCE}"
# What the coworker KNOWS is fixed at session start (MEMORY-SPEC §7.1): a
# conversation's knowledge must not shift underfoot — a fact it referenced ten
# turns ago cannot silently vanish — and the system prompt is the cached prefix,
# so the facts are processed once instead of re-sent every turn. Deletions reach
# NEW conversations; the UI says so rather than pretending otherwise.
remembered = memory_store.list(scope=Scope.GLOBAL)
if ws is not None:
remembered += memory_store.list(scope=Scope.WORKSPACE, workspace=str(ws))
block = format_memories(remembered)
block = render_memory_block(remembered)
if block:
instructions = f"{instructions}\n\n{block}"
@@ -285,9 +366,12 @@ def build_engine(
registry.register(propose_plan_tool())
# Per-turn ephemeral context, appended to the latest user message since mid-thread system
# messages aren't reliable across providers. Two producers: the plan-mode reminder (mode can
# flip mid-session, so it's checked each turn, not baked into the instructions) and the live
# directory list (orphan Cowork can gain folders mid-session; Cowork/MyHelper only).
# messages aren't reliable across providers. Three producers: the plan-mode reminder (mode can
# flip mid-session, so it's checked each turn, not baked into the instructions), the live
# directory list (orphan Cowork can gain folders mid-session; Cowork/MyHelper only), and the
# memory-SAVING notice (same reason as plan mode — the switch flips either way mid-chat).
# Note what is NOT here: the memories and the user's rules. Those are knowledge, fixed at
# session start (§7.1).
roots_context = (
(lambda: render_context(root_list))
if root_list and agent.family == "knowledge"
@@ -300,6 +384,11 @@ def build_engine(
parts.append(_PLAN_MODE_CONTEXT)
elif permissions.mode is Mode.DISCUSS:
parts.append(_DISCUSS_MODE_CONTEXT)
# Only the SAVING switch is per-turn (§4.3): it governs an action, not
# knowledge, so it must bite the moment the user flips it. What the coworker
# knows stays fixed for the session — see the instructions built above.
if memory_store is not None and not _saving_enabled():
parts.append(_MEMORY_OFF_NOTICE)
if roots_context is not None:
ctx = roots_context()
if ctx: