Skip to content

feat(core,server,web): Memory — notes an agent keeps between Sessions, in user and workspace scopes - #144

Merged
hiyouga merged 48 commits into
mainfrom
feat/agent-memory
Aug 11, 2026
Merged

feat(core,server,web): Memory — notes an agent keeps between Sessions, in user and workspace scopes#144
hiyouga merged 48 commits into
mainfrom
feat/agent-memory

Conversation

@rank-Yu

@rank-Yu rank-Yu commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Adds agent_state/memory/: Markdown notes an agent maintains itself with the ordinary file tools. Each scope carries its own MEMORY.md index; 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:

  • User scope (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.
  • Workspace scope (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.
agent_state/memory/
├── user/                         # user scope, created with the agent (no marker: it stands for no path)
│   ├── MEMORY.md                 # this scope's index: one line per memory, links relative to the scope
│   └── prefers-pnpm.md
└── my-app-a81f32c4/              # created on the first Session in that Workspace
    ├── .workspace                # the Workspace path this key stands for
    ├── MEMORY.md
    └── testing-conventions.md

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). user is 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 .workspace marker 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 Workspace my-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_at in frontmatter — which scope a memory belongs to is expressed by its directory, so there is no type field (a type: 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 to memory.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 memory section with its index — plus memory.workspace_prompt, a ## Workspace memory section, 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:

key injected carries
memory.prompt always {{MEMORY_USER_INDEX}}
memory.workspace_prompt persistent Workspace only {{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 literal user; 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 to CWD (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.md itself, 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 # Environment and the Workspace Memory Key line right after that heading). Nothing is ever spliced in automatically, and the assembled prompt is recorded in session_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 (AgentMemoryConfigDto reports 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's MEMORY.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.enabled round-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 .workspace path 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:

  • View renders the body (frontmatter stripped — the header already shows those fields) in a right drawer on desktop and a bottom sheet with half/full snap points on narrow screens, the chat page panels' interaction.
  • Delete confirms, removes the file, and the server prunes its index lines.
  • Edit opens a bridging modal first — the memory's file path, a requirement field, and a live preview of the generated prompt, the same shape as the skill import modal, with copy and open-a-new-chat actions — then jumps to a new chat with this agent through the same draft-cache route, the prompt prefilled and the requirement filled in (or left trailing to complete in the composer). A Workspace scope's chat (edit and import alike) also pins that Workspace, so the Session is injected with the very index it is about to change.

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

@rank-Yu rank-Yu changed the title feat(core,server,web): Workspace Memory — long-term notes an agent keeps between Sessions feat(core,server,web): Memory Aug 5, 2026
@rank-Yu rank-Yu changed the title feat(core,server,web): Memory feat(core,server,web): Memory — notes an agent keeps between Sessions, in two scopes Aug 5, 2026
rank-Yu and others added 9 commits August 7, 2026 01:05
…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
rank-Yu force-pushed the feat/agent-memory branch from 3f177e4 to fe720a5 Compare August 7, 2026 08:33
…, 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
@rank-Yu rank-Yu changed the title feat(core,server,web): Memory — notes an agent keeps between Sessions, in two scopes feat(core,server,web): Memory — notes an agent keeps between Sessions, in user and workspace scopes Aug 7, 2026
rank-Yu and others added 16 commits August 7, 2026 03:06
…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
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
rank-Yu and others added 5 commits August 7, 2026 07:03
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
rank-Yu marked this pull request as ready for review August 7, 2026 15:12
Copilot AI balanced review requested due to automatic review settings August 7, 2026 15:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

rank-Yu and others added 17 commits August 10, 2026 02:10
…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
hiyouga merged commit 13838e4 into main Aug 11, 2026
4 checks passed
@hiyouga
hiyouga deleted the feat/agent-memory branch August 11, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants