Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e80c913
feat(core,server,web): Workspace Memory — long-term notes an agent ke…
rank-Yu Jul 31, 2026
f116ede
fix(web): lift the Memory index above the Workspace selector
rank-Yu Jul 31, 2026
66cf7ff
Revert "fix(web): lift the Memory index above the Workspace selector"
rank-Yu Jul 31, 2026
438b20b
fix(web): stop the Memory file list spinning a skeleton with no Works…
rank-Yu Jul 31, 2026
fecb1f3
feat(web): move Workspace Memory into its own page, built like traces
rank-Yu Aug 4, 2026
f5c6a32
fix(web): give the memory switch its own tab back, not the Prompt page
rank-Yu Aug 5, 2026
31a92e8
docs(core): fix two memory.ts comments that gave the wrong reason
rank-Yu Aug 5, 2026
8f51bc2
feat(core,server,web): give Memory an Agent-level scope beside the Wo…
rank-Yu Aug 5, 2026
fe720a5
style(web): rewrap agent-settings TabKey union after rebase conflict …
rank-Yu Aug 7, 2026
79e6d8f
feat(core,server,web): reshape Memory — user scope, per-scope indexes…
rank-Yu Aug 7, 2026
1f78884
feat(core,web): cap injected Memory indexes at 200 lines; trim the Me…
rank-Yu Aug 7, 2026
7a89b9e
fix(web): label workspace memory groups, say 工作区 in the zh Memory copy
rank-Yu Aug 7, 2026
590218a
feat(core,server,web): Memory prompt lives in the template; render st…
rank-Yu Aug 7, 2026
7415cea
feat(core): Memory prompt gains delete-wrong-topics and absolute-date…
rank-Yu Aug 7, 2026
cbfbb4e
feat(core): rewrite the Memory prompt in template-example form
rank-Yu Aug 7, 2026
9c67e19
feat(web): memory edit gets a bridging modal before the chat jump
rank-Yu Aug 7, 2026
6520ae1
feat(web): collapsible memory scope groups; workspace groups titled b…
rank-Yu Aug 7, 2026
b657860
fix(web): memory edit button matches the view button's look
rank-Yu Aug 7, 2026
2794b42
fix(web): memory tab description talks function, concisely
rank-Yu Aug 7, 2026
1b7dc09
fix(web): memory tab description — cite the storage path, restore the…
rank-Yu Aug 7, 2026
c4d4b8d
feat(core,server,web): Memory prompt returns to config keys, editable…
rank-Yu Aug 7, 2026
bcf5f3b
feat(core,server,web): one memory prompt with a [workspace_memory] re…
rank-Yu Aug 7, 2026
4a3c674
feat(core,server,web): back to two memory prompt keys; editor stays flat
rank-Yu Aug 7, 2026
1fb3303
fix(web): memory description says what the agent saves, not that it r…
rank-Yu Aug 7, 2026
c4e4318
fix(web): workspace empty-state copy matches the description's phrasing
rank-Yu Aug 7, 2026
dd942b9
docs(changelog): Memory entry catches up with the shipped tab
rank-Yu Aug 7, 2026
1a30495
feat(core): condense the default Memory prompt to the house style
rank-Yu Aug 7, 2026
31402b3
fix(web): drop the duplicate all-empty state on the Memory tab
rank-Yu Aug 7, 2026
3d6d781
feat(web): click-to-insert placeholder reference for the memory prompts
rank-Yu Aug 7, 2026
780a544
fix(web): memory placeholder chips insert into the last-focused field
rank-Yu Aug 7, 2026
c9f9eda
fix(core,server,web): Memory bug sweep — legacy-config fallback, syml…
rank-Yu Aug 7, 2026
fe3465a
feat(web,server): Memory tab import bridge, persistent collapse, mobi…
rank-Yu Aug 10, 2026
30c7038
refactor(core,server,web): drop the memory type field — scope is the …
rank-Yu Aug 10, 2026
7523580
refactor(web): rename memory-edit-source to memory-chat-prompts
rank-Yu Aug 10, 2026
578b2b3
fix(web): order the agent-card counts like the settings tabs
rank-Yu Aug 10, 2026
4b6b64b
feat(web): rename memory import to add, slim the bridge drafts
rank-Yu Aug 10, 2026
d24252e
refactor(core,server,web): static memory prompts — literal dirs, Envi…
rank-Yu Aug 11, 2026
7cd88cc
docs(core,web): state the index char cap exactly — 25,000 characters …
rank-Yu Aug 11, 2026
d11f6d9
fix(core): an emptied memory prompt no longer silences the workspace …
rank-Yu Aug 11, 2026
40f4303
fix(web): keep the settings page mounted on memory config writes, ref…
rank-Yu Aug 11, 2026
ea2b60e
refactor(core,server,web): align the two memory Directory lines — key…
rank-Yu Aug 11, 2026
8336207
docs(web,docs): tighten the workspace memory key placeholder description
rank-Yu Aug 11, 2026
35b63ec
docs(web,docs): spell out the none values — no more "(none — …)" elli…
rank-Yu Aug 11, 2026
3f1ec27
refactor(web): drop the agent card's memory icon button — the stat-li…
rank-Yu Aug 11, 2026
433d450
refactor(core,docs): teach per-line index brevity, keep the char back…
rank-Yu Aug 11, 2026
7fd2fba
refactor(core,web,docs): memory placeholders renamed and self-contain…
hiyouga Aug 11, 2026
dd6c267
Merge remote-tracking branch 'origin/main' into feat/agent-memory
hiyouga Aug 11, 2026
7c13902
refactor(core): wrap the Skills section's SKILL.md path in inline code
hiyouga Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions changelog/unreleased/2026-08-07-memory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Memory: what an agent keeps between Sessions

An agent now has a long-term store it maintains itself: Markdown notes under `agent_state/memory/`, kept in two scopes — one for the user and one per Workspace — each with its own `MEMORY.md` index that enters the context, topic bodies read on demand. It covers what a later Session cannot re-derive from the Workspace — who the user is and their standing preferences, project decisions with their reasons, conventions, entry points into external systems.

Memory is not context compaction. Compaction preserves one Session's short-term working state; Memory is what survives the Session ending.

## Scope and layout

There are two scopes, both belonging to one agent and 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.

Each scope carries its own `MEMORY.md` index — one line per memory, `- [Title](file.md) — hook`, links relative to the scope directory — and different agents never share Memory even in the same Workspace.

```text
agent_state/memory/
├── user/ # user scope (created with the agent)
│ ├── MEMORY.md # this scope's index
│ └── 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
```

`user` is safe to reserve because every generated workspace memory key is `<base>-<8 hex>` and so always carries a hyphen — a hyphen-free name can never be produced.

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: two symlinks to one directory resolve to a single key, and moving or renaming a directory makes it a new Workspace — the old Memory stays on disk under the old key rather than following a path that no longer exists.

A **temporary** Workspace gets no Workspace scope. One is allocated per session, so no later session would ever run there to read it back — memory 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/` — rather than whether the caller passed a Workspace explicitly, because a subagent inherits its parent's Workspace as an explicit argument, temporary ones included.

A topic file is a semantic subject, not one per Task, Session or date, and 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. In the body, `[[name]]` links related memories and corrections / decisions carry **Why:** and **How to apply:** lines. Never saved: credentials, task progress, unconfirmed guesses, or facts the code and Git history already state.

## 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, then a `## User memory` section with its index (`{{USER_MEMORY_INDEX}}`) — plus `memory.workspace_prompt` (a `## Workspace memory` section with `{{WORKSPACE_MEMORY_INDEX}}`) when the Session runs in a persistent Workspace, so a temporary Workspace is never told about a scope it does not have. Both prompts are per-agent config, editable on the Memory tab, organized by Markdown headings like the template's other sections. The `User Memory Dir` line is the literal pattern `<app_data_dir>/agents/<agent_id>/agent_state/memory/user`, resolvable from the Environment section; the `Workspace Memory Dir` line renders resolved via `{{WORKSPACE_MEMORY_DIR}}`, because its final segment — the workspace memory key — is a path hash the model could never compose itself.

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 long-line indexes — past a cap a truncation note tells the model to open the full `MEMORY.md` itself. The default Memory prompt declares the line cap and asks for index lines under ~150 characters; the character backstop lives only in code.

A template without `{{MEMORY}}` injects nothing — an agent created before Memory, for instance. The Memory tab reports this and offers inserting the placeholder (before `# Environment`, the position the default template gives it) as an explicit one-click action; nothing is ever spliced in automatically. The assembled prompt is recorded in `session_meta`.

Reading, writing and deduplicating are the model's own work through the ordinary file tools — the Harness decides where Memory lives and keeps writes inside it, nothing more.

## Managing it in the Web App

Agent settings gain a **Memory** tab between Prompt and Runtime: the switch on top (written immediately, not joining the tab's Save, so turning Memory off never drags an unrelated half-finished edit along), then 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. Groups collapse on header click, the same convention as the skill library's, and the collapse state is remembered in the browser per user × Project × Agent. Rows show the memory's name, description and date.

The tab also edits the two memory prompts in place, below the groups. Rows are deliberately read-only, with icon-only actions like the skills tab's. **View** (eye) opens the rendered body (frontmatter stripped — the header already shows those fields) in a right drawer on desktop and a bottom sheet on narrow screens (the chat page panels' interaction, half / full snap points); **Delete** (trash) confirms, removes the file and mechanically drops its `](<file>)` lines from that scope's `MEMORY.md`, so the index never lists a file that is gone; **Edit** (pencil) opens a bridging modal first — a requirement field and a live preview of the generated prompt, the same shape as the skill import modal — 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 memory's edit chat also pins that Workspace, so the editing Session is injected with the very index it is about to change.

Each group header also carries an **Add** entry, a ghost text action to the collapse arrow's left — the models page's per-group convention — whose modal is 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), bridging into a new chat where the agent organizes the content into that scope. Both bridge drafts stay deliberately minimal, naming only the target and the ask — the save mechanics already live in the agent's Memory prompt. And the agents list cards' stat line shows the memory count behind a brain icon next to the other counts, deep-linking to the Memory tab.

Turning Memory off keeps every file and leaves the tab fully usable; it only stops Memory from entering the context and from preparing directories for new Sessions.

The API is under `/api/projects/:p/agents/:a/memory` — overview, per-scope file listing, file read, file delete, and the idempotent placeholder insert; deliberately no content write or rename, since content changes go through chat. The memory prompts ride the ordinary config route. It never accepts a path: a file is addressed by agent, scope key and a name inside that scope, each validated and then re-checked for containment after resolution.
2 changes: 2 additions & 0 deletions changelog/unreleased/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
- [2026-08-06] Models: Thinking Machines Lab's Inkling joins on OpenRouter and Fireworks AI, Fireworks AI gains DeepSeek V4 Flash 0731, and the OpenRouter + SiliconFlow GLM-5.1 gateway listings are delisted (Z.AI direct stays; existing Project configs unaffected); OpenRouter prices refreshed from the models API on 2026-08-07 (Inkling cached input $0.17, four drifted rows corrected); agenthub-models skill v11. ([details](2026-08-06-model-catalog-inkling-dsv4-flash-0731.md))

- [2026-08-06] Release tooling: repo versions realigned with the shipped 0.2.1, and the release workflow now refuses a tag push whose version does not match `package.json` (the drift that made every dev build nag about updates); the bump is documented as a release-prep step. ([details](2026-08-06-release-version-guard.md))

- [2026-08-07] Memory: an agent keeps long-term notes between Sessions under `agent_state/memory/` — a user scope (`memory/user/`, read by every Session) plus one directory per Workspace, each with its own `MEMORY.md` index; only the indexes enter the context (capped at 200 lines / 25,000 chars per scope), bodies read on demand. A temporary Workspace gets the user scope only. The template's `{{MEMORY}}` placeholder expands to the agent's own `memory.prompt` (+ `memory.workspace_prompt` in a persistent Workspace), both editable on the Memory tab; a pre-Memory template injects nothing until its one-click placeholder insert. Agent settings gain a Memory tab: memories grouped by scope with view / delete (index lines pruned) / edit-via-chat. ([details](2026-08-07-memory.md))
15 changes: 14 additions & 1 deletion packages/core/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
loadProjectConfig,
projectDir,
goalFilePath,
resolveSessionMemory,
resolveModelRef,
sessionScratchpadDir,
systemConfigPath,
Expand Down Expand Up @@ -260,12 +261,23 @@ export class Agent {
this.state.projectId,
this.state.agentId,
);
// Memory for this Session: null when the Agent has Memory off; a temporary Workspace gets
// the user scope only (nothing written against it could ever be read back). Reads the
// current indexes every time, like the vault and Skills above.
const memory = await resolveSessionMemory({
root: this.state.root,
projectId: this.state.projectId,
agentId: this.state.agentId,
workspaceDir,
enabled: this.state.systemConfig.memory?.enabled !== false,
});

// The assembled system prompt goes both to the LLM and into session_meta (so the
// Trace can audit the actual effective value). The vault only injects **key names**
// into the prompt (so the model knows which API keys are available); values only
// go into the subprocess environment. Skills only inject metadata (name and
// description); the model reads the body on demand via shell.
// description); the model reads the body on demand via shell. Memory likewise injects
// only its index; topic bodies are read on demand.
const systemPrompt = assembleSystemPrompt(
this.state,
sessionEnvironment(workspaceDir, sessionId, {
Expand All @@ -276,6 +288,7 @@ export class Agent {
}),
Object.keys(vault),
installedSkills,
memory,
);

const rt = await this.buildRuntime({
Expand Down
Loading