feat(core,server,web): Memory — notes an agent keeps between Sessions, in user and workspace scopes - #144
Merged
Conversation
…eps between Sessions
Adds `agent_state/memory/`: Markdown notes an agent maintains itself with the file
tools, kept per Workspace under one shared index. Only the index enters the context;
topic bodies are read on demand. Scope is Project + Agent + Workspace.
Core: state/memory.ts derives a workspace key (`<safe-basename>-<8 hex sha256 of the
real path>`), so symlinks to one directory collapse to one key while a moved directory
becomes a new Workspace. A temporary Workspace gets no Memory — the test is the
directory's location (under an agent's `workspaces/`), not whether the caller passed a
Workspace, because a subagent inherits its parent's as an explicit argument. A
`.workspace` marker records the path a key stands for, which the Web App needs since
the key itself is a hash. system_config gains `memory.enabled` / `memory.prompt`, and
the default template gains `{{MEMORY}}`, rendered with `{{MEMORY_DIR}}` and
`{{MEMORY_AGENTS_MD}}` at Session creation and empty when Memory is off or the
Workspace is temporary.
Server: MemoryService + /api/projects/:p/agents/:a/memory (overview, index, per-
Workspace file listing, file read/write/delete, rename). No route accepts a path — a
file is addressed by agent, workspace key and a name inside that Workspace, each
pattern-checked and re-checked for containment after resolution.
Web: a Memory tab between Prompt and Runtime — the switch, a Workspace selector, the
shared index pinned above that Workspace's topic files, and a Markdown editor with
create / rename / delete. Renaming or deleting a topic file also repoints or removes
the exact `](<key>/<file>)` index links, so the index never lists a file that is gone.
Existing agents are untouched: no `memory` section and no `{{MEMORY}}` placeholder
means nothing is injected until they adopt it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KvVGoQVVDWzVPbHCnY645t
The shared index is Agent-level — one file covering every Workspace — but it was pinned as the first row of a Workspace-scoped list, so it reappeared unchanged at the head of every Workspace and the list contradicted its own heading. It now sits between the switch and the selector, so the tab reads top-down as a narrowing scope: Agent-level switch and index, then the Workspace selector, then that Workspace's topic files. The list holds topic files only, with no row that behaves differently from its neighbours. This also fixes an empty state: with no Workspace memory directory at all, the list container still rendered under the pinned index row while `files` stayed null, leaving a skeleton that never resolved. The list is now inside the has-a-Workspace branch, so that agent gets the "no Workspace memory directory yet" line and nothing else. The one place the two levels meet is that opening the index lands on the selected Workspace's group heading. With the index above the selector that coupling is no longer implied by position, so the card states it, and flags a Workspace with no group in the index yet — which doubles as telling the user this Workspace's memories have not been registered in the index. Verified against a running server: the index renders above the selector for a Workspace with a group and one without, and a fresh agent with no memory directory renders the empty line instead of a skeleton. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvVGoQVVDWzVPbHCnY645t
This reverts commit 49bd6f6. Dropped at the repo owner's request: the Memory index goes back to being pinned as the first row of the Workspace file list, as it shipped in the feature commit. Note for whoever picks this up: the reverted commit also fixed an empty state — with no Workspace memory directory at all, the file list renders under the pinned index row while `files` is still null, so its skeleton never resolves. That bug returns with this revert and is worth fixing on its own, independently of where the index sits.
…pace An agent with no Workspace Memory directory yet — no Session has run in a persistent Workspace — left the Memory tab showing a loading skeleton that never resolved, under the pinned index row. `files` starts null and the fetch effect returns early when no Workspace is selected, so null meant two different things: "the request is in flight" and "there will never be a request". The list rendered the skeleton on null alone, so the second case spun forever. Adds `fileListState`, which separates them: no Workspace selected renders nothing below the index (topic files belong to a Workspace, so the list holds the Agent-level index alone), and the index row gains `last:border-b-0` so it does not leave a divider hanging over the container's edge when it stands by itself. The index keeps its current position at the head of the list; this changes only what renders underneath it. Verified against a running server on both paths — a fresh agent shows the "no Workspace memory directory yet" line with zero pulsing skeletons on screen, and an agent with a Workspace still lists its topic file under the index. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvVGoQVVDWzVPbHCnY645t
The Memory tab lived in the Agent settings page's max-w-3xl column, so
everything stacked: switch, directory path, Workspace dropdown, file list,
actions, then an 18-row editor far below the fold. Editing meant scrolling
down and scrolling back up to switch files, the Workspace <select> flattened
Agent -> Workspace -> topic file into a control showing one Workspace at a
time, and memory for another Agent was three navigations away — even though
memory is content, like traces and benchmarks, not runtime configuration.
Memory now has its own page at /memory, using the same skeleton as
traces-page.tsx and benchmark-page.tsx: a directory tree on the left, the
detail on the right, lazy fetch on expand, loading/empty/error inline in the
node that owns them. The tree is three levels rather than two, because unlike
trace files (which are bare indices) topic files have titles and types worth
showing; the editor then gets the full height of the right pane, which is what
this page is actually for. The shared index is the Agent node's first child —
one index covers every Workspace, so it belongs to the Agent, not under any
one of them. A "+" on each Workspace row creates a topic file there, matching
how the traces page hangs its import control off an Agent node.
Tree state lives in the page rather than in each node, unlike the two sibling
pages: this one writes, and rename/delete rewrite the Agent's index while one
of its *files* is selected, so the index has to be reachable from there.
The Agent-level switch stays configuration and moves to the Prompt tab, right
under the placeholder reference — it decides whether {{MEMORY}} in the
template above it resolves to anything. Its "no {{MEMORY}} in this template"
warning now reads off the editor buffer, so it clears the moment the
placeholder is inserted instead of after a save. Both failure modes (switched
off, no placeholder) are also badged on the Agent's node in the tree, so
memory that cannot reach the model is visible before you open it.
fileListState is gone: it existed because the flat layout had to render
something below the index with no Workspace selected. A tree node holds its
own state, so the case it covered no longer arises.
Verified in the running app with a seeded data root: the tree, the empty
Workspace, the disabled-Agent badge, and create/rename/delete end to end —
the index repoints on rename and drops the entry on delete, leaving the other
entries untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiFzTtintX2JR99SfxzWgP
Putting the Agent-level memory switch at the bottom of the Prompt tab was the
weak part of the last commit, and it failed the only test that matters: the
person who wrote the feature had to ask where the switch was. The tab is a
14-row AGENTS.md editor plus a 12-row system_prompt editor plus a 13-row
placeholder table, so the switch sat below all of it, under a label that says
nothing about memory.
It also coupled two unrelated things. PromptTab's submit() packs every changed
key into one request, so flipping the switch forced whatever was half-typed in
either textarea to be persisted along with it — and editing a prompt carried
the switch. The old Memory tab wrote immediately and had neither problem.
So Agent settings keep a Memory tab between Prompt and Runtime, holding
configuration only: the switch, the "enabled but your template has no
{{MEMORY}}" warning, the memory directory with its Workspace count, and the
button into /memory. The switch writes on change, as it did before. The
content — index and topic files — stays on the Memory page.
This is the split the rest of the repo already uses: agent config lives in a
settings tab (vault, schedules), content lives on a top-level page (skills,
benchmarks, traces).
Verified against a running server: seven tabs with Memory among them, no
switch left on Prompt, the toggle persists on change, and — typing into
system_prompt and then flipping the switch leaves the saved system_prompt
untouched, which is the coupling this undoes. The button lands on
/memory?agentId=default_agent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiFzTtintX2JR99SfxzWgP
Both state a correct conclusion and then justify it with something that is not true. A comment whose reasoning is wrong is worse than no comment: the next person changes the code against it. isTemporaryWorkspace said temporary Workspaces get no Memory because "the directory is gone by the time anything could be recalled". It is not gone. Deleting a Session removes its Traces and scratchpad (sessions.ts) and nothing else; workspacesDir has exactly two consumers — its own definition and createTempWorkspace — so nothing prunes workspaces/tmp-xxxxxxxx, ever. The real reason is that one is allocated per Session, so no later Session runs in that directory and memory keyed off it would be write-only storage. That holds whether or not the directory survives, which is the point. ensureWorkspaceMemoryDir described the marker rewrite as following a changed path, "e.g. the directory was reached through a different symlink". That case cannot occur: realPathOrResolve canonicalizes before the key is computed, so a symlink resolves to the same real path, the same key, and the same directory with an identical marker — the rewrite branch never fires. What it actually does is repair a marker that is missing, truncated or hand-edited, and it can never be a rename path because the key is a hash of the very path the marker records. Comments only; no behavior change. Core: 619 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TiFzTtintX2JR99SfxzWgP
…rkspace one
Memory was Workspace-scoped only, which left two gaps. A Session in a
temporary Workspace got no Memory at all — not even read access to the index —
so a throwaway chat could not recall anything the agent had ever learned. And
`type: feedback` is usually about the *person*, not the directory, yet it had
to be filed under whichever Workspace happened to be open and was invisible
from every other one.
Both are now the Agent scope, `memory/agent/`: what stays true wherever the
agent works. Every Session reads it; a Session in a temporary Workspace writes
there and nowhere else, which is also the only honest destination it has —
one temp Workspace is allocated per Session, so a bucket keyed off it would be
write-only storage nothing could ever read back.
`agent` is safe to reserve because every generated workspace key is
`<base>-<8 hex>` and so always carries a hyphen; a hyphen-free name cannot be
produced. That one invariant is what lets the scope reuse everything: it is a
directory under `memory/` like any other, addressed through the same
`workspaceKey` parameter, so the server needed no new routes and the tree no
new node type. The only asymmetries are that it carries no `.workspace` marker
(it stands for no path) and that its directory is created on demand, since it
belongs to the Agent rather than to a Session that has run.
`memory.prompt` splits into two config keys. Substitution has no conditionals,
so a single block would have to name `{{MEMORY_DIR}}` and then hand a temporary
Workspace a placeholder with no value. Instead `memory.prompt` is always
injected and carries the new `{{MEMORY_AGENT_DIR}}` plus the index, and
`memory.workspace_prompt` is appended only in a persistent Workspace and
carries `{{MEMORY_DIR}}`. The scope-choice rule lives in the second half for
the same reason — a Session with one scope has no choice to make and never
sees it. `{{MEMORY_DIR}}` keeps its meaning, so a hand-edited prompt does not
silently change what it renders.
Verified against a real Session on both paths: a persistent Workspace renders
both halves plus the choice rule, a temporary one renders the Agent half alone
with no `Workspace memory directory` line, and `memory/` holds `agent` plus the
workspace key. In the app the Agent-level node sits between the index and the
Workspaces, and writing to it through the API creates the directory that no
Session had created yet.
An agent whose config predates `workspace_prompt` degrades to Agent-scope-only
rather than losing Memory, so nothing needs migrating.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiFzTtintX2JR99SfxzWgP
…resolution Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
rank-Yu
force-pushed
the
feat/agent-memory
branch
from
August 7, 2026 08:33
3f177e4 to
fe720a5
Compare
…, edit via chat
The user scope replaces the agent scope (memory/user/, type: user), and the
single shared memory/AGENTS.md gives way to one MEMORY.md index per scope,
injected fenced by [user_memory_index] / [workspace_memory_index] marker pairs.
memory.enabled becomes the only on/off channel: a template without {{MEMORY}}
still gets the block, spliced in before # Environment at render time (the same
contract as the win32 Shell-line fallback), so pre-Memory agents need no
migration and the enabled-but-not-injecting state is gone. Agent init creates
memory/user/ with an empty index; Session creation does the same for a
persistent Workspace's scope.
The API drops to read + delete (scopes/:key/files; the workspaces/ segment,
file PUT, rename and the index endpoints are gone) — deleting a file also
prunes its ](file) index lines server-side. The standalone /memory page is
removed; the settings Memory tab now lists every memory grouped by scope with
view (rendered drawer), delete (confirm + index prune) and edit, which jumps
to a prefilled chat with this agent through the skill-import draft route —
a Workspace memory's edit chat also pins that Workspace.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
…mory tab Each scope's index now injects at most 200 lines (one memory per line by convention); past the cap a truncation note tells the model to open the full MEMORY.md itself, and the file on disk is never touched. Documented in the configuration pages and the changelog entry. The Memory tab keeps only its top description (now also correcting user memory's reach: this agent's sessions, not global, and folding in the off-switch semantics); the switch-card hint, the memory-directory path line and the user-scope hint repeated it and are gone. The view drawer renders the body without its frontmatter block — the metadata header already shows those fields. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
Workspace groups now read 工作区记忆:<name> / Workspace memory: <name>, mirroring the 用户记忆 / User memory heading above them; the zh description and empty-state copy switch from the raw word Workspace to 工作区, matching the rest of the zh UI (临时工作区, 按工作区分组). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
…rips, never splices
The # Memory section (with a ## Workspace memory subsection) is now ordinary
visible template text in the default system prompt, the same status as the
# Vault / # Skills statements; memory.prompt / memory.workspace_prompt config
keys, the {{MEMORY}} placeholder and the before-#-Environment splice fallback
are all gone. Four placeholders remain for the dynamic parts. Rendering only
ever removes: memory.enabled off strips the whole section by heading range,
and a Session with no Workspace scope strips just the subsection — a
temporary Workspace is never told about a directory it does not have, and the
Prompt tab shows exactly what runs.
Adoption is explicit instead of implicit: the overview reports whether the
template carries the section, and POST …/memory/template-section (surfaced as
a one-click hint in the Memory tab) idempotently inserts the default section
before # Environment through the ordinary config write.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
…s rules Two conventions carried over from the reference memory prompt: a topic that turns out to be wrong is deleted together with its index line in the same round, and dates are written absolute (YYYY-MM-DD) — relative ones mean nothing to a later Session. Configuration docs updated in both languages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
The section now leads with a fenced frontmatter example instead of describing the format in prose — showing the shape beats telling it — followed by a compact type glossary, the index contract, and the hygiene rules, mirroring the reference memory prompt's structure. Also carries over its ask-what-was- non-obvious rule for 'remember this' requests. The two scope paragraphs and their fenced indexes are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
Clicking edit no longer teleports straight into a new conversation. A modal — the same shape as the skill import modal — shows the memory's file path, a requirement field, and a live preview of the generated prompt, with copy and open-a-new-chat actions; the requirement, when given, fills the prompt's trailing what-to-change line. The drawer's edit button routes through the same modal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
…y basename Each scope group — user memory and every workspace — now collapses on header click, the same header/height-transition convention as the skill library groups (chevron, hover highlight, inert rows while collapsed). Workspace group titles drop the 工作区记忆 prefix and show just the directory basename, with the full path beside it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
Row and drawer edit buttons drop the primary variant — edit is a peer of view, not the row's main action; the modal's open-a-new-chat button keeps it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
The index/injection internals leave the tab copy; what stays: the agent accumulates memories in chat and can be asked to remember or change things, user memory spans every session while workspace memory is per workspace, and the switch only disables — files are kept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
… edits-in-chat clause Matches the sibling tabs' convention of naming where the data lives, and the restored clause explains up front why rows have no inline editor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
… on the Memory tab
The template goes back to a {{MEMORY}} placeholder and the prompt text to
memory.prompt / memory.workspace_prompt (keeping the template-example form) —
but the invisibility that motivated the template-section detour is fixed at
the root: both prompts are now edited in a collapsible section of the Memory
tab, through the ordinary config write. The two-key split keeps the
conditional Workspace half out of temporary-Workspace sessions without any
render-time structure editing; heading-range stripping is gone.
A template without {{MEMORY}} injects nothing — no splice fallback — and the
one-click adoption action now inserts the placeholder itself
(POST …/memory/template-placeholder). AgentMemoryConfigDto reports effective
prompt values with the built-in defaults folded in.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
…gion; plain editor memory.workspace_prompt folds into memory.prompt: the Workspace part now sits inside a visible [workspace_memory] … [/workspace_memory] region of the one prompt — rendering keeps its content (markers stripped) in a persistent Workspace and removes the whole region in a temporary one, so the old two-key conditional survives with a single editable text. The region tag joins the shared marker list. The Memory tab's prompt editor is a plain bordered section now — no collapse — with a single textarea; AgentMemoryConfigDto drops workspacePrompt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
Reverts the [workspace_memory] region merge: the field boundary is the guardrail — with memory.prompt and memory.workspace_prompt as separate keys there is no marker pair for a hand edit to break, and the conditional injection needs no syntax inside the text. The Memory tab keeps the plain (non-collapsible) editor, now with the two labeled textareas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
…ecords 自动记住内容 read like indiscriminate recording of the conversation and gave 记住 no real object; 自行记下值得保留的信息 names both the judgment and the target. English mirrored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
The prompt editor is flat (not collapsible), scope groups collapse and are titled by basename, the view drawer strips frontmatter, and edit goes through the bridging modal before the chat jump. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
Roughly halves the prompt without dropping a rule: the fenced frontmatter example stays (it carries the format), every rationale clause goes, and overlapping statements merge — matching the terseness of the surrounding default-prompt sections. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
Each scope group already renders its own empty text inside the card; the global EmptyState below the groups repeated the user-scope line between the groups and the prompt editor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
The Prompt tab's convention comes to the Memory tab: a reference card lists
the four memory placeholders with descriptions, each chip inserting at the
cursor of the field it belongs to (user tokens into the main prompt,
workspace tokens into the addendum), execCommand-first to keep the undo
stack. Both textareas go mono like the system-prompt editor.
Also fixes the Prompt tab's template placeholder list, which still carried
the four inner memory placeholders from the template-section era — the
template-level entry is {{MEMORY}}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
The fixed token-to-field mapping goes: a chip now inserts at the cursor of whichever prompt field was focused last (main by default) — chip clicks steal focus, hence last-focused rather than current. The two placeholder lists merge into one, the workspace-only tokens noting their scope in the description. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
…ink hardening, index-guard casing
Review findings (internal Opus pass + external report), all with regression
tests:
- memorySection falls back to the built-in prompts when the memory keys are
missing (the config DTO already reported them as effective), so the
one-click placeholder insert on a pre-Memory agent actually injects;
an explicitly emptied prompt still disables its half.
- Workspace placeholders substitute over the whole joined block — written
into the main prompt they resolve (blank without a Workspace) instead of
leaking literally — and {{MEMORY}} expands last, so index content the model
wrote can never smuggle another template placeholder into a second pass.
- memory-service: scope keys may start with _ (core generates _site-<hash>);
the index guard is casing-proof (memory.md resolves to MEMORY.md on
macOS/Windows); topic listing accepts any non-dotfile *.md (non-ASCII names
the model writes are manageable) while still excluding non-regular files;
reads lstat first — a symlinked topic file 404s instead of following the
link out of memory/, and a raced delete 404s instead of 500ing; the scope
directory itself is realpath-checked against <memory/>/<key>, so a
symlinked scope 404s; index pruning matches ](./f), ](<f>) and titled link
forms.
- Memory tab: a failing per-scope listing degrades to an empty group instead
of blanking the tab, and config writes report back to the settings page via
onConfigChanged, so a later Prompt-tab save no longer reverts the inserted
placeholder from stale data.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG
rank-Yu
marked this pull request as ready for review
August 7, 2026 15:12
…le sheet view, card count - Each scope group header gains an Import action: the edit modal's bridge shape with a required content-or-source field and a live prompt preview, jumping to a new chat (workspace scopes pin their workspace); buildMemoryImportPrompt is pure and unit-tested alongside the edit prompt. - Collapse state persists in localStorage per user x project x agent (only collapsed keys stored; vanished scopes pruned on load). - The memory view follows the chat panels' responsive split: right Drawer at >=1024px, bottom Sheet with half/full snap points below; open flag and content are separated so the Sheet's exit animation keeps its content. - Agent list cards show a memory count (topic files summed over scope directories, sharing memory-service's isTopicFileName rule) deep-linking to the Memory tab; AgentSummary carries memoryCount. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzR2G8MqPAXJkR5Cthr7N2
…directory The frontmatter type (user|feedback|project|reference) duplicated what the directory already expresses and only fed a badge in the UI. Frontmatter is now name / description / updated_at; the default Memory prompt trades the type glossary for one worth-saving line; the DTO and the type badges go away. A type: line left in an earlier file parses as an unknown field and is ignored — no migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzR2G8MqPAXJkR5Cthr7N2
The module now builds both bridge drafts (edit and import); the old name is a leftover of the edit-only stage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzR2G8MqPAXJkR5Cthr7N2
memory / tools / skills / vault / schedules, so the stat line and the tab bar read in one order; session count and last-modified stay at the ends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzR2G8MqPAXJkR5Cthr7N2
The generated drafts repeated mechanics the agent's Memory prompt already carries (file path, index/updated_at upkeep, frontmatter rules). Edit now names only the memory and the trailing requirement line; add names the scope by kind (user / this workspace) and carries the content. The scope-header action reads "Add" instead of "Import" everywhere, code names included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzR2G8MqPAXJkR5Cthr7N2
…ronment dir line, heading sections, 25k char cap
The memory prompts are now 100% static instruction text with the two
indexes as their only injection points. The user directory is the literal
pattern <app_data_dir>/agents/<agent_id>/agent_state/memory/user (resolved
by the model from Environment, like the Skills paths); the workspace
directory is a concrete per-Session value and rides the template's new
"- Workspace Memory Dir: {{WORKSPACE_MEMORY_DIR}}" Environment line next
to CWD, rendering "(none — …)" in a temporary workspace or with Memory
off. {{MEMORY_USER_DIR}} / {{MEMORY_DIR}} are gone.
The [user_memory_index] / [workspace_memory_index] marker fences are gone
too — the block is organized by ## User memory / ## Workspace memory
headings like the template's other sections, and the two tags leave the
shared marker list.
Injected indexes now cap at ~25k characters after the 200-line cap, as a
backstop for indexes whose few lines are enormous (cut at a line
boundary; mid-line only when a single line alone exceeds the cap). Both
caps are declared in the default Memory prompt via interpolated
constants, so the model keeps the index short before ever hitting them.
The one-click placeholder insert now also adds the Environment line (and
the Memory tab hint fires when either placeholder is missing); the web
placeholder references, docs and changelog follow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013A3SSVgwmU8ryDp5dUgWns
…total, not "~25k" The cap is an exact constant applied to the whole index, and "~25k" left both facts ambiguous (approximate? per line?). The default prompt now interpolates the raw number as "(25000 characters total)", and the web placeholder references, docs and changelog say "at most 200 lines and 25,000 characters total". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013A3SSVgwmU8ryDp5dUgWns
…half memorySection returned early when memory.prompt was explicitly emptied, dropping a non-empty workspace_prompt with it — while the join below already handles an empty user half. The two halves are edited independently on the Memory tab, so clearing one must never silence the other; the block only disappears when every half that would render is empty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013A3SSVgwmU8ryDp5dUgWns
…resh card counts after tab mutations Two staleness/data-loss fixes around agent settings: - The Memory tab's switch toggle, placeholder insert and prompt save called the settings page's load(), which cleared data to show the skeleton — unmounting the tab tree and losing any unsaved prompt edits. They now refresh through a keepStale variant that updates the page's config copy in place; identity changes and whole-state replacements (import / config reset) still clear. - Mutations that move an agent card's counts never refreshed the list provider, so the Agents page kept stale numbers until a full reload. Memory delete, skill install/uninstall, vault add/delete and schedule create/delete now call reloadAgents(), and the config save path also reloads when builtin tools change (the card's tool count). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013A3SSVgwmU8ryDp5dUgWns
… in Environment, pattern in the prompt
The workspace section's Directory line now mirrors the user section's:
both read <app_data_dir>/agents/<agent_id>/agent_state/memory/<…>, the
user one ending in the literal `user`, the workspace one in
<workspace_memory_key>. The Environment line accordingly carries just
that segment — `- Workspace Memory Key: {{WORKSPACE_MEMORY_KEY}}` — the
directory name the code and docs already call the workspace memory key
(workspaceMemoryKey()), with the same "(none — …)" values as before.
{{WORKSPACE_MEMORY_DIR}} is gone, and the docs' <workspace_key> spelling
is unified to <workspace_memory_key> so the term greps as one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013A3SSVgwmU8ryDp5dUgWns
"目录名(记忆路径模式的最后一段;…说明值)" said the same thing three ways; the chip and the docs table row now read "当前工作区的记忆目录名; 临时工作区或关闭记忆时为 (none — …)", the English mirrors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013A3SSVgwmU8ryDp5dUgWns
…psis The ellipsis form made readers guess. The placeholder chips now say just "none"; the docs table and paragraphs spell both actual values, (none — temporary workspace) and (none — memory is off). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013A3SSVgwmU8ryDp5dUgWns
…ne count already links there The right-side button group carried a dedicated Memory icon button while the stat line's memory count deep-links to the same tab; one entry point is enough. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013A3SSVgwmU8ryDp5dUgWns
…stop code-only, air out the memory prompts The index contract now asks for lines under ~150 characters (CC-style) and declares only the 200-line cap; the 25,000-character backstop stays in code — when it fires, the truncation note says so. Blank lines separate the description / Directory / Index lines in both scope sections so they no longer render as one glued paragraph. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013A3SSVgwmU8ryDp5dUgWns
…ed, memory tab affordances aligned
Prompt side: the memory prompts' injection points are now {{USER_MEMORY_INDEX}} /
{{WORKSPACE_MEMORY_INDEX}}, and the workspace directory renders in place via
{{WORKSPACE_MEMORY_DIR}} — the "- Workspace Memory Key:" Environment line is gone,
so hasMemoryPlaceholder / the one-click insert handle {{MEMORY}} alone. The scope
sections label their directories "User Memory Dir" / "Workspace Memory Dir", paths
wrapped in inline code.
Web side: the agent card's memory stat icon is a brain; the memory rows' view /
edit / delete are icon-only buttons like the skills tab; each scope header's Add
is a ghost text entry to the collapse arrow's left, the models page's group
convention (chevron in its own button at the far right).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMegGEQTAURPBsneZCRkvz
# Conflicts: # packages/core/src/state/agent-state.ts
Same treatment as the memory prompts' directory lines: the default template's long literal paths read as code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QMegGEQTAURPBsneZCRkvz
hiyouga
approved these changes
Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
agent_state/memory/: Markdown notes an agent maintains itself with the ordinary file tools. Each scope carries its ownMEMORY.mdindex; only the indexes enter the model context, topic bodies are read on demand. Memory is not context compaction — compaction preserves one Session's short-term working state, Memory is what survives the Session ending.Two scopes
Both belong to one agent and are never shared with another:
memory/user/) — what stays true wherever the agent works: who the user is, their standing preferences, reference material not tied to one codebase. Every Session reads it.memory/<workspace_memory_key>/) — facts about one Workspace: project decisions, feedback received, pointers into its external systems. Sessions of one agent in one Workspace share it; different Workspaces keep their topic files apart.The workspace memory key is
<safe-basename>-<8 hex of the real path's sha256>— identity is the directory itself, with no dependence on Git, so two symlinks to one directory resolve to a single key while moving or renaming a directory makes it a new Workspace (the old Memory stays on disk under the old key).useris safe to reserve because a generated key always carries a hyphen: a hyphen-free name can never be produced.A temporary Workspace gets no Workspace scope. One is allocated per Session, so no later Session would ever run there to read it back — a bucket keyed off it would be write-only storage. Such a Session still gets the user scope, which is where anything it learns belongs anyway. The test is the directory's location — anything under an agent's
workspaces/— because a subagent inherits its parent's Workspace as an explicit argument, temporary ones included.The
.workspacemarker exists because the key is a hash and cannot be read back into a path — without it the Web App could only ever label a Workspacemy-app-a81f32c4. It is a dotfile, so it never shows up as a topic.A topic file declares
name(kebab-case, matching the file name) /description(one line used to decide relevance during recall) /updated_atin frontmatter — which scope a memory belongs to is expressed by its directory, so there is no type field (atype:line left in an earlier file parses as an unknown field and is ignored). In the body,[[name]]links related memories, and corrections / decisions carry Why: / How to apply: lines. Memory lives in Agent State and is readable by every Project member who can reach the agent, so credentials and sensitive personal data never belong in it.What reaches the model
Only the indexes, through the template's
{{MEMORY}}placeholder. It expands tomemory.prompt— what Memory is for, the save mechanics in template-example form (a fenced frontmatter example, what is worth saving, the index contract with both injection caps, the hygiene rules), then a## User memorysection with its index — plusmemory.workspace_prompt, a## Workspace memorysection, when the Session runs in a persistent Workspace. Both prompts are edited right on the Memory tab, so nothing about the injection is invisible.The prompts are 100% static instruction text organized by Markdown headings, like the template's other sections — the indexes are their only injection points:
memory.prompt{{MEMORY_USER_INDEX}}memory.workspace_prompt{{MEMORY_INDEX}}Both Directory lines are the same literal pattern
<app_data_dir>/agents/<agent_id>/agent_state/memory/<…>, resolved by the model from the Environment section like the Skills paths. The user section ends in the literaluser; the workspace section ends in<workspace_memory_key>— the one per-Session segment, riding the template's Environment section as the- Workspace Memory Key: {{WORKSPACE_MEMORY_KEY}}line next toCWD(the directory name the code already calls the workspace memory key). Patterns live in prompt text, values live in Environment; the line renders(none — temporary workspace)in a temporary Workspace and(none — memory is off)with Memory off, and nothing references it then.Two keys rather than one because substitution has no conditionals: a temporary Workspace must never be handed the Workspace section — its directory line and the scope-choice rule — so that half is simply not appended there.
A blank index injects an explicit "nothing saved yet" note, and injection is capped at 200 lines per scope, then at 25,000 characters total as a backstop for indexes whose few lines are enormous (cut at a line boundary) — past a cap a truncation note tells the model to open the full
MEMORY.mditself, the file on disk untouched. The default prompt declares the line cap and asks for index lines under ~150 characters, so the model keeps the index short before ever hitting the caps; the character backstop lives only in code.A template without
{{MEMORY}}injects nothing — an agent created before Memory, for instance. Adoption is explicit: the Memory tab shows a hint with a one-click insert (POST …/memory/template-placeholder, an idempotent config write placing{{MEMORY}}before# Environmentand theWorkspace Memory Keyline right after that heading). Nothing is ever spliced in automatically, and the assembled prompt is recorded insession_meta.Deciding what is worth keeping, splitting topics and maintaining the indexes is the model's own work — the Harness decides where Memory lives and keeps writes inside it, nothing more.
Server
MemoryService+/api/projects/:p/agents/:a/memory— overview (switch +templateHasMemory+ one entry per scope, user first), the placeholder insert, per-scope file listing, file read, file delete. The memory prompts ride the ordinary config route (AgentMemoryConfigDtoreports effective values with the built-in defaults folded in). Deliberately read + delete only: content changes go through a chat Session where the model keeps frontmatter and index in step. The one write the API performs is mechanical — deleting a file also drops its](<file>)lines from that scope'sMEMORY.md, so the index never lists a file that is gone; a plain-prose mention survives.No route accepts a path: a file is addressed by agent, scope key and a name inside that scope, each pattern-checked and then re-checked for containment after resolution. The user scope goes through the same routes under the key
user, created on demand (it belongs to the agent, not to a Session that has run); a Workspace key with no directory still 404s.memory.enabledround-trips through the existing config route.Web App
Agent settings gain a Memory tab between Prompt and Runtime. Top to bottom: the switch (writes on change rather than joining a tab-level Save, so turning Memory off never drags an unrelated half-finished edit along); a hint bar with a one-click placeholder insert when the template lacks
{{MEMORY}}; every memory grouped by scope — user memory first, then one group per Workspace, titled by its directory basename with the full.workspacepath beside it, newest activity first, each group collapsing on header click (the skill library's convention), the collapse state remembered in the browser per user × Project × Agent; and the two memory prompt editors with their own confirm-first Save.Each group header carries an Add action — the edit modal's shape with a required content-or-source field (pasted text, a file path or a URL; the agent reads sources itself) and a live prompt preview — bridging into a new chat where the agent organizes the content into that scope. Both bridge drafts stay deliberately minimal, naming only the target (the memory's title / the scope by kind) and the ask: the save mechanics — frontmatter, index upkeep — already live in the agent's Memory prompt.
Rows show name, description and date, and are deliberately read-only:
The agents list cards' stat line shows the memory count next to the tool / vault / schedule / skill counts, deep-linking to the Memory tab.
Turning Memory off keeps every file and the tab fully usable; it only stops Memory from entering the context and from preparing directories for new Sessions.
🤖 Generated with Claude Code
https://claude.ai/code/session_01A5HjinDmtgqQ7jiAXagrbG