Skip to content

feat(memory): long-term memory as a personal wiki that lights up the knowledge base - #55

Draft
lyingbug wants to merge 39 commits into
mainfrom
cursor/plan-long-term-memory-wiki-e57e
Draft

feat(memory): long-term memory as a personal wiki that lights up the knowledge base#55
lyingbug wants to merge 39 commits into
mainfrom
cursor/plan-long-term-memory-wiki-e57e

Conversation

@lyingbug

@lyingbug lyingbug commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Description

Implements per-user long-term memory, end to end: storage, layered settings, recall in chat, agent tools and brief, background extraction and consolidation, the illumination overlay on the knowledge graph, and the memory UI. docs/长期记忆功能规划.md carries the design, opens with a per-item coverage table, and closes with the defects that were fixed and the ones that remain.

The idea is to stop treating memory as one more retrieval source and build it out of the wiki machinery this repo already has.

Memory is wiki-shaped. Memories live in memory_pages — slug, markdown body, [[links]], aliases, revisions — so people read and edit them the way they already read a wiki. Three layers: append-only memory_notes carrying the message ids that justify them, deduplicated memory_pages as the only thing that reaches a prompt, and a graph made of link arrays plus anchors.

Anchors make the knowledge base personal. memory_anchors bridges a memory to a knowledge-base wiki page, filled in from retrieval citations (free) and by resolving the entity names an extraction proposed. From that one table: the wiki graph shaded by how much of each page you have actually engaged with (unlittouchedfamiliarmastered, plus flagged for pages you disputed), a bridged graph showing where your understanding attaches to the organisation's, a per-folder mastery metric, and anonymised aggregates over which pages are asked about but thin.

Two hard constraints, both honoured. Zero Neo4j: graph edges are JSON arrays in the main database and all graph maths runs in Go. Lite is a first-class target: the capability test is DB_DRIVER=sqlite plus an empty REDIS_ADDR, never the product edition, and the whole feature is verified on both databases.

How a memory gets written

The write mode ships as explicit_only: saying "记住……" in a conversation stores that statement verbatim, with no model call at all. Above that, gated_auto runs a pure-rule gate, debounces into a single background extraction per session, then consolidates — deduplicating by normalised hash and merging near-duplicates.

The line between those modes is whether the user said something or asked for it to be remembered. "我是wizard,一个程序工程师" is durable context for the extractor to weigh; "记住我是wizard" is an instruction to store it. The marker has to open a clause, so "I don't remember my password" is nobody's fact.

Every write — extraction, the agent's memory_remember, the memory editor, the API — passes the same content rules on the one path they all share: an instruction is refused, a credential is refused, and direct identifiers follow the workspace PII policy.

Background work carries what the request knew: the session owner scope it was permitted to read under, the agent whose overrides shaped the decision, and the model the turn used. None of that is recoverable in a worker, and each absence had made the feature silently do nothing.

One home, in the product's own style

Memory has a single surface: Settings → Account → 长期记忆, holding what is remembered, what is waiting to be confirmed, how it connects, and the choices that govern any of it. There is no sidebar entry and no page of its own — memory is personal data one reviews occasionally, like message history, which this product keeps in settings. A shortcut sits in the avatar menu beside the other personal entries.

memory_one_home_settings_and_workspace_policy.mp4

The workspace screen is a ceiling for everyone and now reads as one. The two screens looked interchangeable for a structural reason: the personal layer can set seven keys and five of those are also workspace keys, so the "basics" of both were the same five rows. A setting key can now carry per-level wording — personal 写入方式 is workspace 允许的最大写入方式, with help that says what it forbids members from doing.

Workspace memory policy, with rows worded as limits and a note saying this sets a ceiling

Styling follows the dialog rather than TDesign's defaults: the same row shape, type scale and control column as the other sections, the quiet standing note instead of a coloured alert, the card treatment the model list uses, the shared card menu, t-empty for empty states, the product's green in the graph rather than TDesign's blue, and debounced save-on-change with a toast exactly as ChatHistorySettings does it.

A memory card

Other design decisions worth review

Recall costs no model call and no new infrastructure. Resident memories come from an indexed query; relevant ones are scored lexically in Go with CJK bigrams. The plan called for a hidden vector KB per memory space; implementation showed that answered the wrong question — a space holds hundreds of short strings, so scoring them in Go takes microseconds, while a hidden KB per user costs a row to filter out of every user-facing query and a hard dependency on a configured embedding model. Section 6.4 records the reversal.

Illumination is pure functions. Heat, state and coverage are computed over plain slices. Expressing them as SQL (power(), ln(), GROUP BY ROLLUP) works on Postgres and fails outright on Lite, so the repository only runs portable SELECTs. Both engines produce identical numbers, measured rather than assumed.

Memory-borne prompt injection is treated as a first-class threat. Extraction reads only role=user turns — never documents, wiki pages, search results or tool output — so a poisoned document cannot implant an instruction that survives every future session. Preferences take effect solely through whitelisted structured fields. Free text is sanitised and injected as labelled data.

Forgetting is load-bearing. Typed half-lives, decay exemptions, archival rather than deletion, per-space caps, an opt-in retention purge that also removes what pointed at the page, and a sweep on a ticker so it behaves the same with and without Redis. Strength is a function of elapsed time since last use, which makes the sweep idempotent.

Nothing here can break a conversation. Recall runs under a deadline and degrades to "no memory this turn"; anchor recording, extraction and the agent brief are best-effort; a failure anywhere in the subsystem loses memory, never the answer.

Scope

The plan's coverage table is authoritative, and a "known issues" section lists what is not fixed, with the cost of each. The largest is that the insights half of the feature has no UI; the rest are a semantic question about which anchors belong in the bridged graph, relation weights not reaching the ranking boost, a process-local consolidation lock, and two interaction slips.

Not implemented and listed as such: the chat-side surface (@记忆, a "remember this" button, the used-memories drawer, memory citations), new-session continuation suggestions, the wiki page "your memory" sidebar, manual anchoring UI, an insights view, audit logging, POST /memory/import, and all of Phase 5.

Settings and code with no behaviour behind them are deleted rather than shipped dark: four dead switches, the unreachable space settings layer, six API client functions whose endpoints have no UI, a task_pending_ops row written on every extraction that nothing ever read, and three stored-but-never-consulted dependencies.

Also fixes a pre-existing blocker found on the way: comparing the PostgreSQL and SQLite schemas column by column turned up three missing tables and nine missing columns, one of them tenants.api_principal_config, which GORM writes on every insert — meaning a fresh Lite database could not create a workspace at all.

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 📚 Documentation update
  • 🧪 Test
  • 🎨 Refactor
  • ⚡ Performance improvement
  • 🔧 Configuration / Build / CI

Related Issue

Implements the 知识库形态 → 探索知识库与 Memory 结合的应用场景 item in docs/ROADMAP.md.

Testing

This feature was unusable seven times, always the same way: something declared but never wired, with the tests entering below the seam that was broken. The plan lists all of them. That pattern shaped the tests:

  • The write path is entered at the gate. write_path_test.go drives ConsiderSession against real repositories on SQLite and asserts what is in the database afterwards. The earlier end-to-end tests called the memory API directly and so entered below the gate that was refusing everything.
  • Background tasks are checked for what a request would have given them. background_context_test.go pins the workspace on the context, the space owner's user layer and the payload's agent layer in the settings resolution, and that the transcript is read under the requester's session scope — including the channel case that was dead, a session that is not the owner's, and a trigger with no scope at all.
  • The content rules are pinned on the shared path. write_rules_test.go asserts instructions and credentials are refused and identifiers masked, that whole identifiers are masked, and that decay matches its half-life and does nothing on a second same-day sweep.
  • Privacy arithmetic is pinned. The insights tests assert a lone dissenter among ten readers is suppressed, and that the untouched-page list is capped with the remainder counted.
  • Cleanup is pinned. A purge test asserts revisions, anchors and merged notes go with the page — and caught that revisions key on page_id while anchors key on memory_page_id, so the first version of that fix deleted nothing.
  • Translation references and every selectable option resolve in all four locales. A type-check cannot see a string that comes out as a raw key at runtime. Both guards were verified to fail when a key is removed, and writing the second immediately found an unlabelled option and an incorrect list I had guessed rather than read.
  • Scope isolation is pinned, not assumed. Assertions in memory_scope_test.go prove cross-space and cross-workspace reads, writes, deletes, hit counting, de-duplication, space lookup and the insights aggregate stay confined.

go test ./... is green across all packages; vue-tsc --build, vite build and the locale tests pass.

End-to-end on both databases. A 28-assertion HTTP suite covering settings resolution, lazy space creation, writing, links and backlinks, revisions and revert, optimistic locking, the graph, anchors, layered locks and clamping, memory switched off, export and forget passes identically on Lite (SQLite, no Redis) and PostgreSQL.

Verified through the product, not the API. A Lite instance ran with a local stand-in chat provider so the full conversation path executes, including the schema-constrained extraction call. Confirmed live, against the running deployment after the final changes: an instruction refused through the manual write API, an 18-digit identifier stored as [id], an anchor into an unreadable knowledge base answered 404 and one missing its target 400, and a real chat turn producing extracted 1 candidate notes and a consolidated pipeline-sourced memory. Earlier rounds confirmed a direct request recalled in a brand-new conversation, and the review inbox path.

A fresh conversation recalling the memory stored in an earlier one

An automatically extracted candidate waiting in the review inbox

The bridged graph is the feature's central idea made visible: solid nodes are the person's own memories, dashed satellites are the knowledge-base pages their understanding is anchored to.

Bridged memory graph with dashed knowledge-base satellites anchored to memory nodes

Defects found and fixed on this branch

Measured, not estimated, where a number appears.

  • Nothing was written to memory in any default configuration. The default mode defers to a direct request nothing could issue; gated_auto refused to run without a dedicated extraction model while that setting promises the conversation's model when blank; agent mode reached the write path not at all.
  • Extraction could never work for the IM, tenant API-key or embed channels. It read the transcript through an owner-scoped helper that decides from the caller's principal and tenant role, and a worker has neither: web fell through, the other three returned "session not found", retried and were dropped. Three of the four supported channels were dead however memory.channels was configured.
  • Background tasks ran without the context that queued them. Settings resolved from tenant and space only, so a write mode chosen in personal settings was invisible: the task read the default and reported success in 10ms having done nothing. Past that gate the first tenant-scoped query failed outright. Both also applied to the decay sweep.
  • Decay compounded, so memories vanished in weeks. Each daily sweep re-applied a factor computed from the total age; a profile crossed the archive threshold on day 45 rather than the ~999 days its half-life describes.
  • Every content rule on the write path was bypassable, so an agent could persist "ignore your system prompt" as a preference — which is then prepended to every later turn — and a credential was stored verbatim whatever the deny patterns said.
  • PII redaction left the tail of an 18-digit national ID in the text, because alternation binds looser than concatenation and neither branch carried both word boundaries.
  • Anchoring required no read access to the knowledge base it wrote into, so anyone could colour any knowledge base's coverage and insight aggregates.
  • The insights k-anonymity gate protected readers rather than dissenters: ten readers and one objection published a report that made the objector identifiable in a small workspace.
  • Reverting silently won over a concurrent edit, and a retention purge left revisions, anchors and merged notes pointing at a deleted page — which is how a purged memory keeps colouring the overlay.
  • Export always failed with a 401 in a blank tab. Pinning an archived memory un-archived it, and pinning twice conflicted. task_pending_ops grew forever.
  • The ranking boost could never match wiki content (anchors store the slug, every candidate was an id), and illumination vanished on drill-down (the neighbourhood loads dropped the overlay parameter).
  • The card menu opened behind the settings dialog. Setting labels rendered as raw keys in all four languages, and relation options as raw tokens. The Memory entry was unreachable by clicking. The settings resolver folded defaults as a layer, making booleans unturnable in the restrictive direction. Every frontend query parameter was silently dropped. Seven response interfaces described the API while nothing used them, so responses were any to the template — including a confirm field no Go struct had. The write gate stopped at the first direct request. "Remember" matched anywhere in a sentence. Search fired per keystroke. Bridged mode could report "showing 40 of 25". Plus four dead settings, backlink maintenance bumping the page version, a startup Invoke that killed the process on boot, one shared stopword surfacing unrelated memories, and an injection guard matching one phrasing rather than the family.

Not verified, and stated as such in the plan: extraction quality and cost against a real model on real conversations, read-path latency under load, long-run convergence of a memory space. One open question is recorded rather than settled: settings and data management share one scrollable dialog section, which two reviewers independently called heavy; the alternative reintroduces a page with no navigation entry.

Checklist

  • git diff --check origin/main...HEAD passes
  • Changed source files are formatted (gofmt)
  • Targeted tests for the changed packages/components pass
  • Diff-scoped lint passes where applicable
  • Full-repository checks were run — go test ./..., frontend type-check, build and locale tests green
  • Self-reviewed the code
  • Added/updated tests covering the change: scope isolation, the write path, background task context and session scope, the content rules, decay, privacy arithmetic, purge cleanup, and translation coverage
  • Updated related documentation — coverage table, every divergence recorded in place, why the feature kept breaking, and the audit findings both fixed and remaining
  • Breaking changes are clearly called out — none; memory writes are limited to direct requests by default and every touch point in existing code paths is additive and guarded

Screenshots / Recordings

Included above.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

lyingbug and others added 9 commits August 7, 2026 09:43
Plans a per-user long-term memory subsystem that reuses the wiki page /
folder / link-graph / revision machinery, and introduces memory anchors as a
bipartite bridge between a user's memory pages and the knowledge base wiki so
the KB graph can be rendered "lit up" per user.

Covers the three-layer memory model (episodic notes / semantic pages / graph),
principal-scoped memory spaces, the read path (no extra LLM calls) and the
debounced async write path, illumination + coverage metrics, the reverse loop
that feeds anchor signals back into wiki issues, API and frontend surface,
four-level config gating, privacy and memory-borne prompt-injection defenses,
cost budgets, phased delivery, evaluation thresholds and open questions.

Also records why the previous Neo4j conversation memory (removed in 380e371)
failed and how each failure mode is avoided.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
…-memory-wiki-e57e

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
…pling

Rebased the plan onto v0.7.2 (18739bb) and reworked it around two hard
constraints the design must now satisfy.

Zero Neo4j: drops the "Neo4j as an optional accelerator" escape hatch and the
open question about Cypher-based multi-hop recall. Graph relationships are
expressed with relational tables plus JSON link arrays in the main database,
and all graph math runs in Go. The glossary keeps the Neo4j document entity
graph only as a boundary contrast, with an explicit note that the memory
subsystem neither reads nor writes it.

Lite as a first-class target: adds a runtime-form contract section. The
capability test is DB_DRIVER=sqlite plus an empty REDIS_ADDR, not
handler.Edition, and six binding constraints follow from it -- dual migrations
with a portable type mapping, no dialect-specific SQL, heat and coverage
computed in Go rather than SQL, async work routed through
interfaces.TaskEnqueuer and registered in both the asynq and SyncTaskExecutor
paths, Redis-optional locking with a per-space in-process fallback, and short
transactions to respect SQLite's single writer. Also records a pre-existing
gap: task_pending_ops and task_dead_letters are missing from the sqlite
migrations, which leaves Lite wiki ingest incomplete; fixing it becomes a
Phase 0 prerequisite.

Reworks configuration into a per-feature settings design: ~35 settings across
eight groups, five override levels with per-category merge rules, safety
settings that can only be tightened, effective values that report their source
and lock state, and explicit capability declarations so unavailable features
grey out instead of silently failing. Deployment form now only shifts defaults
and capabilities, never code paths -- notably, Lite is not assumed to run
small models, since it can use remote models just like standard.

Refreshes six assumptions against upstream: query_understand no longer yields
a rewrite on unparsable output, SearchResult carries ContentRevision /
ContentRewritten for merge trust, wiki stats exclude archived pages, ingest
prompts resolve language via ResolveLanguageName, memory migrations move to
000081-000083, and knowledge auto-tagging is noted as a sibling async pattern.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Introduces the storage layer, the layered settings framework and the core
services for per-user long-term memory.

Data model: memory_spaces keyed by (tenant, principal type, principal id) so
one store follows a person across web, OIDC, IM, API and embed rather than
being tied to sessions.user_id, whose format varies per channel. Memory itself
is wiki-shaped -- slugs, markdown bodies, [[links]], revisions -- so people
read and edit it the way they already read a wiki. Notes are append-only with
the message ids that justify them; pages are the deduplicated, injectable unit;
anchors bridge a memory to a knowledge-base wiki page. Migrations are written
for both PostgreSQL and SQLite, and the SQLite chain also picks up
task_pending_ops and task_dead_letters, which had only ever existed on the
Postgres side and left Lite's wiki ingest without its durable queue.

Settings: a single descriptor table declares all 39 keys once -- type, default,
bounds, which layers may set them and how layers combine -- and the resolver is
driven entirely from it. Merge behaviour is per-key rather than global: enable
flags AND together so any layer can veto, budgets take the stricter value so a
narrow layer cannot inflate cost, and privacy keys can only tighten. Resolved
values carry the layer that decided them and the layer that locks them, so a
UI can explain why a control is read-only.

Services: recall assembles resident and query-relevant memories under a token
budget with no model call, scoring lexically in Go with CJK bigrams rather than
through a vector index -- a memory space holds hundreds of short strings, so
this needs no embedding model, no per-user hidden knowledge base and behaves
identically on both databases. The write path is gated by rules before it costs
anything, debounced per session, and produces reviewable notes; consolidation
supersedes contradicting memories instead of overwriting them, and a daily
sweep decays and archives rather than deletes. Illumination and coverage are
pure functions over anchors so both engines produce identical numbers.

Safety: extraction reads only user-authored turns, so a poisoned document
cannot implant a durable instruction; preferences take effect solely through
whitelisted structured fields; free text is sanitised and injected as labelled
data; instruction-shaped and credential-shaped candidates are rejected at write
time rather than at read time.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
…graph

Connects the subsystem built in the previous commit to the places it has to
be useful.

Chat: a MEMORY_RECALL stage runs after query understanding and before
retrieval, so the anchor hints it produces reach reranking and the memory block
is ready when the prompt is assembled. The block is prepended as labelled
background data, once, guarded by a flag because the pure-chat path builds its
prompt before the pipeline runs while the RAG path rebuilds it midway. After
the answer, cited wiki pages become asked_about anchors and the turn is offered
to the write-path gate; both are best-effort.

Agent: four tools -- search, read, remember, forget. Deliberately no tool for
structured preferences or arbitrary field edits, since those steer generation
and belong to the user. None accepts a space identifier, so an agent cannot be
argued into reading someone else's memory.

Tasks: extract, consolidate and decay register on both the asynq mux and the
SyncTaskExecutor. Registering only the former is how a background feature ends
up silently dead on Lite, so the two lists are kept adjacent and commented as a
pair. The decay sweep runs from a ticker rather than the asynq scheduler,
because Lite has no scheduler and a memory store that only forgets in one
deployment form would grow without bound in the other.

Wiki graph: ?overlay=memory decorates nodes with the caller's heat and state.
Additive and opt-in, so a client that never asks sees an unchanged response.

Tests pin the parts where a mistake is invisible: the settings merge rules, the
illumination maths on both the decay and exemption paths, coverage arithmetic,
relevance scoring including CJK, k-anonymity suppression, and the injection
defences. Three defects surfaced this way and are fixed here -- built-in
defaults were being folded as though they were a layer, which made boolean
switches unturnable in the restrictive direction; the de-duplication hash did
not trim after collapsing punctuation, so 'X.' and 'X' were stored twice; and a
single stopword in common was enough to surface an unrelated memory, now
guarded by a query-coverage floor.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
…illumination

The memory centre puts everything a person can do with their own memory on one
page, because the feature only earns trust if 'what does it know about me, and
how do I change it' has a single obvious answer. Four tabs in the order people
ask the questions: what is remembered, what is waiting for approval, how it all
connects, and what it is allowed to do.

The review inbox lets a proposed memory be edited in place before it is
accepted. Forcing a correction through a second dialog is what makes people
reject instead of fix, and a rejected-but-true memory is a worse outcome than a
reworded one.

The settings panel is generated entirely from the descriptor catalogue the API
returns, so a setting added on the backend appears without a frontend change and
can never drift from what is actually enforced. Each row shows where its value
came from and, when a wider layer has pinned it, says so and goes read-only
rather than accepting a click that would do nothing. A basic/advanced split
keeps the first screen to the six questions most people have. The same panel
renders the workspace policy under system settings at the tenant layer.

Knowledge-graph illumination is an opt-in toggle on the existing wiki graph:
nodes are shaded by how much of each page the viewer has actually engaged with,
disputed pages are outlined in the warning colour regardless of age, and a
coverage bar shows how much of the knowledge base is lit. Additive throughout —
the request only asks for the overlay when the toggle is on.

GraphCanvas is a new self-contained force layout used by the memory graph. The
wiki browser's own renderer is left in place and extended instead: extracting
575 lines of entangled simulation, pan/zoom, bloom and drawer state out of a
6.5k-line component, with no visual regression harness available, would be the
riskiest change here for no user-visible gain. Unifying the two renderers is
follow-up work.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
…e schema drift

Three fixes, all found by running the thing rather than reading it.

Backlink maintenance no longer bumps a memory's version. A page gaining an
inbound link because another memory started pointing at it is bookkeeping, not
a revision: counting it as one showed the user a version jump they did not make
and handed an open editor a spurious optimistic-lock conflict. The repository
now has a separate UpdateLinks that writes only the link arrays.

Validation failures return the status that describes them. A stale version is
409 so the editor can reload and show the newer text; asserting a
system-derived anchor relation, or an unknown type or scope, is 400. All three
were 500, which reads as 'the server is broken' for what are ordinary
disagreements about a request.

The SQLite migration chain is brought back in line with PostgreSQL. Comparing
the two schemas column by column, built for real rather than read from
migrations, turned up nine missing columns and three missing tables — and one
of them, tenants.api_principal_config, is on a struct GORM writes on every
insert, so a fresh Lite database could not create a workspace at all. Memory
needed a bootable Lite instance to be verifiable, and the plan already carried
the sqlite task-queue tables as a prerequisite, so the rest of the drift is
closed here too. Deliberately left alone: the pgvector embeddings table, a dead
column whose Go field was removed, and a one-off migration artefact.

Also defers the memory decay sweep's startup Invoke to the end of container
construction. Providers are lazy, but that one eager resolution pulled the
whole service graph up before the data-source scheduler was registered, and the
process died on boot.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Marks the plan as implemented through Phase 4 and records the two places the
implementation deliberately diverged, with the reasoning, rather than quietly
leaving the document describing something else.

Recall no longer uses a hidden vector knowledge base per memory space. The
original reasoning about the vector abstraction was correct but answered the
wrong question: a memory space holds hundreds of short strings, not millions of
documents, so scoring them in Go costs microseconds and the problem the hidden
KB solved does not arise — while its costs (a KB row per user to filter out of
every user-facing query, a hard dependency on a configured embedding model, a
path to validate across ten drivers) are real. The setting is renamed to
memory.recall.relevance_enabled to match.

The wiki graph renderer was extended in place rather than extracted. Pulling 575
lines of entangled simulation, pan/zoom and drawer state out of a 6.5k-line
component with no visual regression harness would have been the highest-risk
change here for no user-visible gain. Two force layouts coexist for now; the
document says so.

The Lite prerequisite section is widened to what building both schemas and
diffing them actually found: three tables and nine columns, one of which meant a
fresh Lite database could not create a workspace at all.

Appendix 2 now separates design verification from implementation verification
and lists the eight defects the latter surfaced, including the settings merge
treating built-in defaults as a layer, backlinks bumping the page version, and
frontend query parameters never reaching the wire.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
@cursor cursor Bot changed the title docs: long-term memory (Memory Wiki) design plan feat(memory): long-term memory as a personal wiki that lights up the knowledge base Aug 7, 2026
lyingbug and others added 20 commits August 7, 2026 15:31
…dit found

A review question -- why is there a lone /platform/memory page owning the
settings? -- turned out to be right, and auditing the whole plan against the
implementation turned up more of the same.

Settings now live in the three places the design named, and only there. Personal
preferences under Settings > Account, workspace policy under Settings >
Workspace, per-agent overrides in the agent editor's conversation section. The
memory centre keeps only a link. Previously the full user-level surface was a
tab on that page and the design document had been quietly rewritten to match,
which is the mistake behind the mistake: the plan stopped being a plan and
became a description of whatever had been built.

The workspace entry was worse than misplaced -- it was unreachable. It sat in
navItems but in no nav group, and the sidebar renders from groups, so the panel
could never be opened.

The memory centre now matches its sibling pages. It was a centred 1240px column
with an h1, a row of filled buttons and four bespoke stat cards, none of which
appears anywhere else in the app; it now fills the content area like AgentList
and KnowledgeBaseList, with a heading row of text icon buttons and counts as a
quiet line.

Four settings are deleted rather than kept: show_used_memories, cite_memories,
member_coverage_visible and auto_file_wiki_issues had no behaviour behind them.
A control that does nothing is worse than a missing one, because the user turns
it on, believes something changed, and has no way to find out otherwise.

Three that deserved implementing instead of deleting are now real. Retention
purge honours retention.days and purge_archived_after_days -- the only path that
deletes rather than archives, and only when an operator asks for it. Anchor
resolution turns the entity names an extraction proposed into real wiki anchors
by exact title, alias or slug match, which is what makes the memory-to-knowledge
link automatic; the conversation's knowledge-base scope now travels with the
extraction so there is something to resolve against. And the agent system prompt
carries a short memory brief: agent mode had memory tools but no way to know it
should reach for them before it knew anything about the person.

Adds the cross-space and cross-workspace isolation tests that Phase 0 claimed
were green but were never written -- seven assertions covering reads, writes,
deletes, hit counting, de-duplication, the insights aggregate and space lookup.
This is the property where a bug is a privacy incident rather than a defect.

The plan document now opens with a per-item coverage table separating what is
implemented, what diverged and why, and what the design promised but the code
does not do -- including the chat-side surface, the wiki page memory sidebar and
audit logging, which are honestly listed as missing rather than papered over.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Nothing was ever written to memory in any default configuration. Three
faults stacked up, and each one hid the next.

The write mode defaults to explicit-only, which declines the automatic
extractor and defers to a direct request instead — but no direct request
could ever arrive. RememberExplicit had no caller and the trigger's
Explicit flag was never set by anything, so the mode meant "never write".
Asking to be remembered now works: ConsiderSession recognises an
imperative in the user's own words and stores the statement verbatim,
with no model call, which is what makes explicit-only free as designed.
Both word orders occur naturally, so the statement may sit on either side
of the imperative.

Switching to gated-auto did not help, because extraction refused to run
without a dedicated extraction model while that setting's own description
promises the conversation's model is used when it is left blank. The
turn's model now travels with the trigger and is the fallback, so the
documented behaviour is the real one.

Agent mode reached the write path not at all — it only received a brief —
leaving memory capture dependent on the model choosing to call a tool.
The same gate now runs after an agent turn, sharing one settings
resolution with the brief.

Declining to write is no longer silent. The reason is logged, because the
first symptom of all of this was a log with no trace of memory in it.

Widen the injection guard while here: RememberExplicit leans on it, and a
pronoun between the verb and its object was enough to slip past. Cover
the phrasing family rather than one phrasing, and pin down that
preferences, which read as directives by nature, are still storable.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
The write path was unreachable in every default configuration and the
tests did not notice, because they exercised the memory API directly and
so entered below the gate that was refusing everything. These enter at the
gate, with real repositories on SQLite, and assert on what is in the
database afterwards. Both fail against the previous behaviour.

Pinned alongside the fix: the message that exposed it is a statement, not
a request, so the default mode declining it is correct. The obvious
over-correction is to store every sentence containing "I am", and that is
worth a test of its own. "Off" still means off, direct request or not.

In the UI the same question had no answer at all — an empty memory list
looks identical whether nothing was said worth keeping or nothing could
ever be kept. The memory centre now reads the effective write mode and
says which of the two it is, with a link to the setting.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Every control in the memory settings panel rendered as its raw key —
"memory.write.mode" where "写入方式" belonged — in all four languages.
The lookup builds "memory.settings.keys.<key>.label", and i18n reads a dot
as a path separator, so it descended into memory → write → mode instead of
finding the single entry named "memory.write.mode". The group headings were
fine, which is why this survived earlier review: they are single words.

The setting keys come from the backend and keep their dots, so the
translations drop them and the component substitutes underscores.

Guarded by tests over the locale files, since the failure is invisible in a
type-check and easy to reintroduce by adding a setting: no translation key
may contain a dot, every one carries a non-empty label, and the four
locales describe the same set.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
The Memory Centre was unreachable by clicking. Its menu entry existed in the
store, but the sidebar renders from a hand-written list of paths that did not
mention it, and the complementary list — which would have caught the omission
by rendering whatever the first one skipped — is computed and never used. So
the page could only be opened by typing its URL, which is how it went
unnoticed: every walkthrough so far navigated directly.

Derive both lists from one array, so an entry the store adds can be absent
from the sidebar only on purpose.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
The plan described the write path as if it worked, which is how the gap
survived: the explicit-only mode had no implementation behind it and the
document did not distinguish "designed" from "reachable" on this point.

Section 6.2 now opens with the direct-request path that mode depends on,
states where the line falls between saying something and asking for it to be
remembered — the distinction the default mode turns on — and records both
faults that made the whole path inert, so the next reader does not have to
rediscover them.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
A queued task runs without the request that queued it, and everything the
request had on its context has to be put back deliberately. Two things were
not, and each failed in its own quiet way.

Settings were resolved from the tenant and space layers only, so a write mode
chosen in personal settings was invisible out here. Picking "always
automatic" for yourself queued an extraction that then read the built-in
default, concluded automatic writes were not allowed, and reported success
having done nothing — 10ms and an empty result, which reads exactly like "the
conversation held nothing worth remembering". The user layer is recoverable
because the space records its owner; the agent layer is not, so the agent id
now travels in the payload alongside the model id.

Past that gate, the first tenant-scoped query failed outright: every
repository reads the workspace id from the context and the task had none. The
restoration goes inside the writer's entry points rather than the task
handler, because the decay sweep arrives from a ticker and never passes
through a handler.

Both faults also applied to the decay sweep, which was therefore ageing
memories against default retention rather than the owner's.

While here, stamp the conversation's traceparent onto the extract and
consolidate payloads. The payloads already embedded TracingContext but
nothing ever filled it, so each task opened an orphan trace named after
itself instead of appearing under the turn that caused it. Every other task
type in the project already does this.

Declining queued work is now logged with the mode that declined it, since the
symptom of all of the above was a task that succeeded and did nothing.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Memory had three front doors — a sidebar entry, a page of its own, and two
settings panels that rendered the same generated form — and no way to tell
which was in charge. It now has one: an account-level settings section
holding what is remembered, what is waiting to be confirmed, how it connects,
and the handful of choices that govern any of it.

The sidebar entry, the route, the page shell and its icons are gone. Memory is
personal data one reviews occasionally, like message history, which this
product keeps in settings; it did not earn a slot in the main navigation. A
shortcut sits in the avatar menu alongside the other personal entries, so it
is reachable without occupying one.

The two settings screens looked interchangeable for a concrete reason: the
personal layer can only set seven keys, and the "basics" of both screens were
exactly the five they share. The workspace screen is now framed as what it is
— a ceiling for everyone, with policy questions first and the operational
knobs behind a link — and says so above the form. Platform invariants, which
belong to no layer, appear only there; on a personal screen they were rows
nobody could act on.

Styling follows the dialog it now lives in rather than TDesign's defaults:
the same row shape, type scale and control column as the other sections, the
quiet standing note used elsewhere instead of a coloured alert, and the card
treatment the model list uses. Settings save as they are changed, like every
other section here — a separate save step reads as safer and is not, since it
adds a state where the form and the effective value disagree with nothing to
say which is which.

Also drops a `confirm` field the frontend sent on "forget everything" that no
backend field ever matched, and the strings left behind by all of the above.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
They were confusing for a concrete reason: the personal layer can set only
seven keys, so the two screens' primary sets were almost exactly the five they
share, rendered from one catalogue with identical wording. Same labels, same
controls, no way to tell which was in charge.

A setting key can now carry level-specific wording, and the five shared keys
use it. "写入方式" on a personal screen is a choice; on the workspace screen it
is "允许的最大写入方式", with help that says what it forbids members from
doing. The distinction is in the words, where the user is looking, rather than
only in the page title.

Memory cards expose their actions through the shared card menu every other card
in this product uses, instead of three small icon buttons of their own, and the
disclosure link that reveals the remaining settings now looks like one.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
A pass over the whole feature looking for the fault that has bitten it five
times — declared but never wired — found three more instances.

The space settings layer was the widest: three keys advertised that they could
be set per memory space, and `UpdateSpaceConfig` implemented it, but nothing
anywhere called it and no screen offered it. The layer is now honestly
described as reserved for shared spaces, which are a later phase, and the
method that could never be called is gone.

Six API client functions had no caller: search and stats duplicate what the
list and space endpoints already return, and anchor management and insights
have no UI at all. They are removed rather than left as an invitation to
assume the feature exists; the plan claimed manual anchoring was possible in
the memory centre, and it was not.

Seven response interfaces existed and described the API accurately while no
function or call site used them, so every response was `any` all the way to
the template. The ones with an endpoint behind them now type their function,
and the call sites drop the annotation that was discarding it — which is the
kind of check that would have caught the `confirm` field the frontend was
sending to a struct that never had one.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Memory labels rendered as raw keys twice, for two different reasons — a key
absent from a locale, and a key shaped so i18n could never resolve it. Neither
shows up in a type-check or a build; the string just comes out wrong at
runtime, in one language, on one screen.

This walks the feature's sources for literal translation references and
resolves each against all four locale files. Verified to fail when a
referenced key is removed.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
…the dialog

The card menu never opened: the popup's `visible` was controlled while its own
click trigger was also enabled, so the two fought and the click did nothing.
The popup owns its state now and is only observed for styling the trigger.

Settings also saved silently and one write per keystroke. The convention in
this dialog is a debounced save with a toast, which matters most on the
multi-select rows where choosing three types was three requests.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
The menu did open; it opened underneath. Card menus are pinned to z-index 99
because that is right on ordinary pages, but the settings dialog sits at 1100,
so from inside it the menu rendered behind the overlay and the button read as
dead. Two earlier attempts at this chased the click handling instead, which was
never the problem.

Card menus opened from the dialog now carry a modifier that lifts them to 3500,
the same level the anchored form popups shown from that dialog already use.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Section 10 described a standalone page on the main navigation and a file list
that no longer exists. It now describes the single settings section, says why a
browser and a settings form share one scrollable surface, and states the cost of
that plainly rather than only its benefit.

Section 11.5 explains why the two settings screens looked like one form — the
personal layer can set seven keys and five of them are also workspace keys, so
the overlap is structural, not a wording slip — and how per-level wording
resolves it.

Also corrects a false claim: the document said anchors could be created by hand
in the memory centre. They cannot, anywhere. They only ever arise
automatically.

New: a section listing all seven "declared but never wired" defects with their
symptoms, what they had in common, and what verification now has to traverse.
The pattern was not carelessness; it was tests that entered below the seam that
was broken.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
**Decay compounded, so memories vanished in weeks.** Strength was the previous
strength multiplied by a factor computed from the memory's total age, so each
daily sweep re-applied the whole decay. With the shipped half-lives a profile
crossed the archive threshold on day 45 rather than the ~999 days its half-life
describes — measured, not estimated. Strength is now a function of elapsed time
since the last use, which also makes the sweep idempotent, and the reference
falls back to created_at rather than updated_at, which the sweep itself moves.
Negligible changes no longer cost a write, which is what the daily cadence was
chosen to avoid in the first place.

**Every content rule on the write path was bypassable.** Instruction detection,
deny patterns and PII handling lived in the two callers that thought of them,
so the agent's memory_remember tool and the memory editor went straight to the
store. An agent could persist "ignore your system prompt" as a preference,
which is then prepended to every later turn, and a credential was stored
verbatim whatever memory.privacy.blocked_patterns said. The checks now sit on
writePageInScope, the one path every write shares.

**PII redaction left the tail of an 18-digit national ID in the text.** The
pattern was `\b\d{15}|\d{17}[\dXx]\b`; alternation binds looser than
concatenation, so neither branch carried both boundaries and the 15-digit arm
always won: `...231X` became `[id]31X`, and digits inside a longer order number
matched too. Grouped.

**Export always failed.** It opened the API path in a new tab, and
authentication is a Bearer header the axios interceptor attaches — a plain
navigation carries none of it, so the button produced a 401 in a blank tab. It
now downloads through axios like every other download here.

**Pinning an archived memory un-archived it, and pinning twice conflicted.**
The server reads an absent status as "active", and neither the list nor the
editor sent one; the list also kept its old version instead of the one in the
response.

**task_pending_ops grew forever.** A row was written per session per debounce
window and never read — only wiki task types are peeked or claimed — while the
coalescing its comment claimed came entirely from the asynq task id. Removed,
with the one thing it could have bought written down.

Tests pin the three backend rules, each of which was invisible to the existing
suite: the shared write path refuses instructions and credentials and redacts
identifiers, whole identifiers are masked, and decay matches its half-life and
does nothing on a second same-day sweep.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
**The ranking boost could never match wiki content.** A wiki anchor stores the
page slug; every candidate the matcher compared against was an id or a title.
Wiki pages are what anchors were designed around, so enabling the boost changed
nothing for them.

**The write gate stopped at the first direct request.** "Remember I use Go — I
also moved to the platform team" stored the first clause and dropped the rest of
the turn. It now falls through to the automatic gate.

**"Remember" matched anywhere in a sentence.** "I don't remember my password
being changed" was stored as a fact about the user, as was "I can never remember
which flag enables it". The marker now has to open a clause — the position that
makes it an imperative rather than a mention — allowing a leading word of
politeness. Stepping back a byte at a time also landed inside the multi-byte
Chinese clause marks, so a trailing ",记住" stopped working; that steps by rune
now. Dropped "note that" as too weak to carry the reading on its own.

**Illumination vanished on drill-down.** The overview graph passed
overlay=memory and the three neighbourhood loads did not, so mastery shading
disappeared the moment a user clicked into a node — the view where "what do I
already know here" matters most.

Also: the relation and extraction-source multi-selects showed raw tokens
(asked_about, user_message) because only setting keys had been translated, and
the memory search box fired a request per keystroke.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Eleven of them, ordered by impact, each with what it costs a user and why it is
not addressed in this branch. Extraction is dead for the IM, API-key and embed
channels; supersede is documented and does not happen; insights have no UI and a
k-anonymity gate measured against readers rather than dissenters; retrieval
anchors do not reach the bridged graph; relation weights are ignored in ranking;
consolidation's lock is process-local; the retention purge orphans rows; the
SQLite column add is not idempotent; and a few interaction and arithmetic slips.

Also lists the unreferenced fields and constants left over. None of them change
behaviour, and every one is a candidate for the next "declared but never wired".

Recorded here rather than carried in my head, since that is precisely what went
wrong the last seven times.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
The earlier guard covered the setting keys subtree only, which is how the
relation and extraction-source multi-selects came to render raw tokens in all
four languages while every check passed.

Writing it immediately turned up one more: memory.privacy.pii_redaction offers
an option no locale labelled. It also caught my own first attempt, which
asserted against a hardcoded list I had guessed rather than read — so the list
is now the values the descriptors actually declare, and a parity test sits behind
it that fails whenever the locales stop offering the same set, whatever that set
becomes.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Adding im, api or embed to memory.channels bought nothing. Extraction read the
transcript through the owner-scoped service helper, which decides from the
caller's principal and tenant role — a background task has neither. For web
sessions it fell through; for every channel-managed session it returned "session
not found", retried three times and was dropped, leaving a log line as the only
symptom. Three of the four supported channels could never write a memory.

Reconstructing a caller identity in the worker would be the wrong repair: the
mapping from a principal to its session scope differs per channel and, for
tenant API keys, depends on the key id. So the scope travels with the work. It
is captured in the request, where it is knowable, and the worker reads the
session under exactly that scope and no other — which also removes the previous
reliance on an empty owner matching by the legacy rule.

Tests cover the channel case that was dead, a session that is not the owner's,
and a trigger that could not establish a scope at all.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
**Anchoring needed no read access to the knowledge base it wrote into.** The
route cannot check it — the id arrives in the body, so KBAccessRead has nothing
to bind to — so anyone could anchor into any knowledge base in the workspace and
colour its coverage and insight aggregates. The handler now verifies the caller
can read it, using the knowledge-base service it was already holding and never
consulting. AddAnchor also validates that a knowledge base and a target were
actually supplied, and no longer answers 200 with a null body when its own
read-back finds nothing.

**The insights k-anonymity gate protected the wrong people.** It measured the
widest population for a target, which asked_about dominates because it is
created automatically for every cited page. Ten readers and one objection
published a contested insight reporting ten people — making the one objector
identifiable in a small workspace. It now counts and reports only those who
corrected or disagreed. The untouched-pages list is also capped, with the
remainder counted, so a large unread wiki no longer answers with one entry per
page.

**Reverting silently won over a concurrent edit.** The write passed no expected
version, which disables the optimistic check. It now guards with the page's own
version, closing the read-modify-write race, and accepts the caller's last-seen
version so an editor cannot revert over a change it never saw; the dialog sends
it and reports a conflict the way saving already did.

**A retention purge left orphans.** Hard-deleting a page left its revisions,
anchors and the notes merged into it pointing at an id that no longer resolves —
which is how a purged memory keeps colouring the illumination overlay. All of it
goes in one transaction now, with the observations marked no longer merged rather
than deleted so the evidence trail survives. Writing the test caught that
revisions key on page_id while anchors key on memory_page_id, so the first
version of this fix deleted nothing.

Also: bridged graph mode reported "showing 40 of 25" because Returned counted
satellites while Total counted memories; and three stored-but-never-read
dependencies are gone, with the unused VectorKBID column now described as
reserved rather than as a feature that exists.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
lyingbug and others added 10 commits August 8, 2026 05:40
Eleven findings became eight fixed and a shorter list of what is left, each with
the cost of leaving it and the reason. Two entries changed character rather than
disappearing: supersede is now described as the behaviour that exists rather than
the one the design promised, and the SQLite column add is recorded as consistent
with every other migration in that directory rather than as a defect to patch in
one place.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Asking against a normal (non-wiki) knowledge base records asked_about anchors —
target kind "knowledge", target ref the knowledge id — and the space stat counts
them. Nothing read them. All four consumers excluded them: the bridged graph
skips anything that is not a wiki page and anything without a memory page id,
illumination is an overlay on the wiki graph, insights filter to wiki pages, and
the ranking boost loaded hints only for anchors tied to a recalled memory page,
which retrieval anchors never have. Four rows written per turn that no code path
could reach.

Ranking is the consumer they belong to: asked_about describes a person's
engagement with the knowledge base, which is exactly what personalised ranking
needs. Hints are now loaded for the space and the knowledge bases in play rather
than through the recalled pages, so an ordinary knowledge base matches on its
knowledge id and a wiki on its slug.

The relation weight each hint already carried is applied instead of discarded, so
a page someone corrected outranks one they merely mentioned — which is what
memory.overlay.relation_weights claims to control.

The bridged graph still shows nothing for an ordinary knowledge base, and
correctly so: it draws memory-to-wiki-page edges and there are no wiki pages. It
says that now instead of presenting an empty canvas.

Also stops two node captions rendering as one unreadable line: repulsion balances
gravity at about a hundred pixels, which is about how wide a dozen Chinese
characters render. A positional separation pass keeps captions apart without
raising repulsion and blowing dense graphs apart, and the full title is available
on hover now that the visible one is truncated.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Two deferred entries resolved. Retrieval anchors staying out of the bridged
graph is now recorded as correct behaviour rather than a gap — they belong to no
memory, so there is no edge to draw — and the remaining hole is stated plainly:
an ordinary knowledge base has no illumination view at all, because illumination
is an overlay on the wiki graph and insights filter to wiki pages. Its anchors do
affect ranking now, which is the part that was dead.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Illumination only ever existed for wiki knowledge bases: the overlay was an
overlay on the wiki graph, coverage counted wiki pages, and insights filtered to
them. An ordinary knowledge base recorded engagement and had nothing to show for
it.

Nothing about the maths was wiki-specific. The repository projection already
took a target kind, and the heat, state and coverage functions are pure over
plain slices, so the change is to stop hardcoding the kind and to project the
other unit: a document, keyed by the id a retrieval anchor already records, and
bucketed by the first segment of its folder path exactly as a wiki page is
bucketed by its first breadcrumb.

A wiki graph carries its overlay inside the graph response because it is drawing
nodes anyway. A document list has no such carrier, so there is now an endpoint
for it.

Insights work on documents too, with the stored file size standing in for a wiki
page's body length. It is coarser — a scanned PDF is large and may still say
little — but the signal is "asked about a great deal, and barely anything here",
and size carries it.

Tests pin that the two unit kinds do not leak into each other's reports, and
that documents bucket by folder the way pages bucket by breadcrumb.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
The lens the wiki graph applies to its nodes, applied to a document list: which
of these has this reader actually engaged with, and how much of the knowledge
base that adds up to.

A dot beside each document title carries the state — touched, familiar,
mastered, or flagged for something disputed. Nothing is drawn for a document
nobody has touched, which is the common case: a column of grey dots would be
noise that also drowns the few marks that mean something. The tooltip says it in
words, with how many interactions and how long ago. The vocabulary is the wiki
legend's, deliberately, because it is one idea seen from two angles and a reader
should not have to learn two sets of colours for it.

One component renders the dot for both the grid and the list, so the two cannot
drift into different vocabularies.

The toggle sits beside the view switch and borrows its shape, since both are
lenses over the same list, and the choice is remembered — someone who wants this
usually wants it every time. Turning it on reveals a coverage bar reading "lit of
total". The whole control is hidden, not disabled, when memory or illumination is
off: a switch that cannot do anything is worse than no switch.

Verified end to end against a real knowledge base with three documents: two
anchored and lit with their states and heat, the third correctly dark, coverage
at two of three, and the insights report naming the unread one.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Chapter 7 described illumination as something that happens to a wiki. Nothing
about the maths was wiki-specific — heat, state and coverage are pure functions
over a slice of anchors, and the repository projection already took a target
kind — so it now covers both, and says where the two differ: the carrier. A wiki
graph folds its overlay into the graph response because it is drawing nodes
anyway; a document list asks for it directly.

Moves "an ordinary knowledge base has no illumination view" out of the known
issues and into the fixed list.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
…bset

An ordinary knowledge base lit up in the document list while the memory graph
stayed empty beside a header counting its anchors. Both filters in the satellite
pass excluded exactly those anchors: anything that is not a wiki page, and
anything without a memory page id — which every retrieval anchor lacks, and
retrieval is the only way an ordinary knowledge base produces one.

The premise of the bridged view is where a person's understanding meets the
organisation's, and that includes what they have engaged with even when no one
memory is responsible for it. So satellites now cover both kinds, drawn with an
edge when a specific memory is anchored to them and without one otherwise. The
legend and the hint say that, instead of promising wiki pages.

Documents are anchored by id, because that is what a retrieval result carries,
and an id is not a label. The graph service has no knowledge service and should
not grow one for a display concern, so the handler substitutes titles after the
fact; a document since deleted keeps its id, which at least says something is
there.

Unattached satellites are capped at thirty, most-engaged first. They are the
numerous kind — one per cited item per turn — and past a few dozen they stop
being a picture of what someone works with.

Verified against real data: two ordinary documents appear under their own titles
as unattached satellites while four wiki anchors keep their edges.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Retrieval anchors staying out of the bridged graph was recorded as correct
behaviour last round, on the reasoning that they belong to no memory and so have
no edge. That reasoning held for the edge and not for the node: they are still
something this person engaged with, and leaving them out meant an ordinary
knowledge base could light up its document list and show an empty canvas beside
a header counting its anchors.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
- Updated GraphCanvas to support double-clicking nodes to center them in the viewport.
- Added a new `panToNode` function to facilitate panning to specific nodes.
- Enhanced the MemoryEditorDrawer with additional fields for memory preferences and improved layout.
- Introduced MemoryPersonalSettingsDrawer for user-specific memory settings, including export and forget options.
- Improved UI components for MemoryGraphPanel and MemoryInbox, including better loading states and user interactions.
- Updated localization files for English and Chinese to reflect new features and UI changes.

These changes aim to improve user experience and functionality in managing and visualizing memory data.
- Introduced a new document outlining the long-term memory restructuring plan, detailing the transition from a complex Memory Wiki to a more user-friendly model focused on "Saved Memory" and "Chat History Memory."
- Updated the MemoryPage and MemoryNote interfaces to include new fields: `saved` and `memory_key`, allowing for better differentiation between user-saved memories and automatically inferred memories.
- Enhanced the memory retrieval system to prioritize saved memories during recall, ensuring they are always considered regardless of type.
- Modified the frontend components to reflect the new memory structure, including updates to the MemoryList and MemorySettings views for improved user interaction.
- Updated localization files for English and Chinese to accommodate new terminology and features related to memory management.

These changes aim to improve user trust and clarity in memory management, ensuring users can easily understand and control what is remembered.
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.

1 participant