Field notes
2026-06-14 — Brain memoize: two failure modes break "remember it"
Note: internal names redacted for public release.
Theme: brain memoize — the remember / think / explore → persist-to-brain → recall-it-later loop.
Status: RESOLVED (2026-06-15). All ranked work shipped to main + security-
reviewed; see "Shipped" below. Two items deferred by decision (durability
DB-backup; per-source ownership RBAC) — captured as design direction, not bugs.
Shipped (merged to main)#
| Item | What | PR |
|---|---|---|
| Mode A | kb_put_page FK in /1on1 → ensureSource before write | v0.23.1 |
| Mode B/B′ | kb_think always cross-checks a distilled kb_search, surfaces uncited hits | #38 |
| open_kb | removed the last markdown-reading KB tool (all KB tools now pure gbrain DB) | #34 |
| soul | kb_memoize is the one write path; dropped /ingest/raw/ steering | #38 |
| standing grant | per-thread, per-writer, 8h, in-memory approval grant | #36 |
| grant security | bound grant to (thread, writer) — fixes cross-user approval bypass | #40 |
| #7a | humanizeBrainError — actionable brain-write errors | #39 |
| #7b | soul baseline: a failed brain write is reported, never papered over | #39 |
| EMBEDDING_DIMENSIONS | 2560→1280 to match zembed-1 | deploy (Bitbucket) |
Mode C (/1on1 scope silo): resolved as by-design — /1on1 memoize is
personal (user-<id>); team knowledge goes via a shared channel. No code change.
Historical investigation detail (Cases 1–2, live-DB forensics, ruled-out hypotheses) is preserved below as the record.
Summary#
Three staging cases, two distinct failure modes, same user-visible symptom ("I told you to remember this, but you don't know it"):
- Mode A — write never lands (Case 1).
kb_put_pageFK-fails on an un-bootstrapped source; agent silently degrades to file writes;trigger_ingestnever runs. The lesson is genuinely absent from the brain. - Mode B — write lands, recall misses (Case 2).
kb_put_pagesucceeds and the page is indexed, non-stale, in the right scope — butkb_thinkreturns a zero-citation "not captured" on a verbose NL query, while a tight-keywordkb_searchfinds it at rank 1. The lesson is present but the agent reports it missing.
Mode A is a write defect (src/knowledge source bootstrap). Mode B is a
retrieval defect (gbrain kb_think recall + no think→search fallback). Fixing
only one leaves the symptom alive.
Case 1 — Mode A: ingest silently fails, recall stays thin#
Trigger#
staging thread example.slack.com/C0XXXXXXXXX/1781076888.704939 (staging, agent
"example-agent"). Manager asked example-agent to pull ITGC audit history from GDrive and
"memoize" it into her brain. Later DM (D0AKKK79DT8/1781412338) probed recall:
"who is Manager, purely from brain" → example-agent returned a one-line stub. Operator
(barock) flagged the whole remember/think/explore experience as not optimal and
shipped the session JSONL (61 files, session dir
-data-workspaces-T1WTF90DS-C0XXXXXXXXX-1781076888-704939) for forensics.
What happened (traced from JSONL, not the Slack surface)#
The 06-14 ingest session (50555749-…jsonl) ran this chain:
gather → workbench google_sheets_read ×2 (only 2 ranges — thin coverage)
store → kb_put_page ×4 → ALL FAILED
"insert or update on table \"pages\" violates foreign key
constraint \"pages_source_id_fkey\""
fallback→ Write raw/*.md + episodic memo project_itgc-ingestion.md
("NOT yet ingested into brain — manager to decide")
ingest → trigger_ingest NEVER CALLED
result → zero brain pages for ITGC → recall is a thin stubA separate 06-10 session (11abec57-…jsonl) proves the pipeline works when
driven correctly: trigger_ingest → "ingested 2 raw file(s); 3 wiki pages
changed; pushed 615b33c". So the machinery is sound; this session bypassed/broke
it and degraded silently — neither the assistant nor the user got a signal that the
brain write hard-failed and the task was incomplete.
The episodic memory layer captured the fallback correctly but recorded "NOT yet ingested" as if it were a clean checkpoint, masking the failure.
Why recall is thin (the user-visible symptom)#
The facts about Manager/ITGC never became queryable knowledge. They live in:
raw/itgc-*.md(staged, never ingested into the wiki index), and- a status memo about pending work (
project_itgc-ingestion.md),
not as entities/* or concepts/* pages the brain can kb_think over. "Who is
Manager" therefore returns one stub line after a whole session about him.
Mode-A defect (write side) — ROOT CAUSE CONFIRMED#
Every kb_put_page inside a /1on1-locked thread FK-fails, because per-user
brain sources are never created.
Confirmed against the live staging brain (PGLite snapshot, 2026-06-14):
- The
sourcestable holds exactlyagent,default,public,shared, and onekb-<label>per installed KB (incl.kb-maria-memory, 5 pages — so the KB source bootstrap is fine; the earlier "missing maria-memory source" hypothesis was wrong). There are nouser-*rows. resolveBrainScope(src/knowledge/scope.ts:56-63): when a thread is/1on1-locked and the lock owner is the caller, write scope becomessourceId = userSourceId(userId)→ e.g.user-u06ensb6pv0….userSourceId()is only ever used to set the write target (scope.ts:58) and classify the gate (gated-dispatch.ts:41). Nothing inserts that source.ensureSources()(brain.ts:275) only bootstrapsbaselineSources()= agent/shared/public +kb-*(brain.ts:263); no per-user source is registered at boot, at lock time, or on write.- The 06-14 ITGC thread was
/1on1-locked (lock + engage markers in the session JSONL). So the write hituser-<barock>→ not insources→pages_source_id_fkeyviolation, ×4.
Control: the Case 2 jot lesson (06-11) was not locked, so it scoped to
shared (which exists) and wrote cleanly. Same agent, same channel — the only
difference is the /1on1 lock. That isolates the cause precisely.
So kb_put_page works in normal channels and breaks in exactly the place you'd
most want durable memory — a focused 1:1 "remember this" session.
Compounding (same as before): the raw Postgres FK error leaks to the agent
(src/knowledge/mcp-tools.ts:138), and no stop-guard catches "intent was a
brain write, zero pages changed" — so Maria degraded to file writes and reported
a clean checkpoint.
Fix (Mode A)#
Register the user source before it's written. Cleanest options, in order:
- In
gatedBrainCall/kb_put_page, ensurescope.sourceIdexists before the op (lazysources_addforuser-*), then insert. Covers every path. - On
/1on1lock acquisition, callensureSources([userSourceId(owner)]). - Have
resolveBrainScope's user-source branch be backed by a guaranteed registration step. Plus: map FK/brain errors to actionable text; stop-guard for failed-intent brain writes.
Case 2 — Mode B: write lands, retrieval misses#
Trigger#
Same lesson, two threads, three days apart:
Thread
C0XXXXXXXXX/1781143233(2026-06-11) — write confirmed. Operator: "patri ini in your brain, do't make the same mistake in the future" (etch this in). Maria wrote it via a singlekb_put_page:- slug
lessons/jot-deployment-pattern, KBlending-business - tool_result:
{status:"created_or_updated", chunks:2, write_through:{written:true, path:".../lending-business/.sources/shared/lessons/jot-deployment-pattern.md"}} - No FK error, no
trigger_ingestneeded (synchronous write-through), no fallback. Maria: "Sudah diukir tajam di brain." Mode A did not occur here.
- slug
Thread
C0XXXXXXXXX/1781168759(2026-06-14) — retrieval missed it. Operator: "what lesson you learn on deploying dashboard with jot to workbench?" Maria rankb_think("What lessons were learned about deploying dashboards with jot to workbench?") → result: "The brain does not contain information about lessons learned…", diagnosticspagesGathered:40, takesGathered:0, graphHits:0, citations:[]. Maria replied "Not captured yet." User: 😞. Only when the operator explicitly said "kb search jot deployment" did Maria runkb_search "jot deployment"→ 1 hit, rank 1, score 1.083, sluglessons/jot-deployment-pattern, page_id 2443,stale:false→ "Found it!"
What this proves#
The page was present the whole time — indexed, non-stale, in the shared scope
both tools hit. Not ingest lag, not wrong KB, not slug mismatch, not an
unopened KB. The defect is retrieval:
kb_think's hybrid retriever gathered 40 candidate pages on a verbose NL query ("deploying dashboards with jot to workbench") and the target page ("Jot Deployment Pattern", about web artifacts) didn't make the top-40 set — swamped by dbt-stub noise;takesGathered:0, graphHits:0so graph and takes paths contributed nothing. The synthesizer then truthfully answered "not in the brain" from an impoverished candidate set.kb_searchwith the tight keyword "jot deployment" lexically matched the title and hit rank 1 instantly.
Mode-B defect (retrieval side)#
kb_thinkrecall miss → false "not captured." A zero-citation think result is reported to the user as authoritative absence. The page exists and is trivially findable by keyword. (gbrainkb_thinkretrieval — candidate-set truncation / ranking on long NL queries.)- No think→search fallback. When
kb_thinkreturns empty/zero-citation, the agent should auto-retry with extracted keywords viakb_searchbefore declaring absence. Today it gives up.src/knowledge/brain-think.ts+src/knowledge/mcp-tools.ts(kb_thinkhandler). kb_thinkis the default recall verb but has worse recall thankb_searchfor known-item lookup. The tool description steers the agent tokb_think("prefer this over kb_search when you need an answer"), which is exactly wrong for "do I have a note on X" — a known-item query where keyword search wins.
Cross-cutting#
Brain/memory tool surface (observed)#
mcp__slaude_kb__:list_kbs,open_kb,kb_list_pages,kb_get_page,kb_put_page,kb_delete_page,kb_search,kb_think,kb_graphmcp__slaude_runtime__trigger_ingest(raw → wiki pipeline)- episodic memory = plain
Writeto…/memory/*.md - write-through:
kb_put_pagewrites synchronously to…/<kb>/.sources/<scope>/<slug>.md(no separate ingest when it works)
Fix direction (NOT yet implemented — collecting cases)#
Write side (Mode A):
kb_put_pagelazily ensures its source exists and retries once on FK error after re-runningensureSources.- Map brain/Postgres errors to actionable text; never let a raw FK message reach the agent.
- Stop-guard: a turn whose intent was a brain write but changed zero pages must not exit reporting success.
Retrieval side (Mode B):
kb_thinkzero-citation → automatickb_searchkeyword fallback before the agent reports "not captured."- Reconsider the
kb_think-first steer for known-item lookups; route "do I have X / what did I learn about X" tokb_search(or a hybrid that runs both and merges). - Investigate
kb_thinkcandidate-set truncation (top-40 gather + ranking) so a present page isn't crowded out by stub noise.
Open questions#
- Mode A: RESOLVED — reproducible on every
kb_put_pageinside a/1on1lock (per-user source never registered). Decide which fix layer (lazy-ensure in the write path vs. ensure-on-lock). - Mode B: is the recall miss query-phrasing-specific, or does
kb_thinksystematically under-ranklessons/*andentities/*pages vs dbt stubs? - Provenance: tag pages
source: ingested|explored|seededso the operator can audit what was actually learned vs seeded.
Fix landed (2026-06-14)#
- Mode A —
brainCallnow callsensureSource(scope.sourceId)before any scope-write op (src/knowledge/brain.ts;isScopeWriteOpadded tosrc/knowledge/gated-dispatch.ts).ensureSourceidempotentlysources_adds the source (swallows source_id_taken / duplicate), cached per id. A/1on1write touser-<id>now self-registers instead of FK-failing. Verified live:sourcesconfirmed to hold nouser-*rows; control = unlocked write tosharedalready worked. - Mode B —
kb_thinkfalls back to keywordkb_searchwhen synthesis returns zero citations, attachingsearch_fallbackhits, so a present page is never reported "not captured" (src/knowledge/mcp-tools.ts). - Tests:
tests/brain.test.ts(Mode A: put_page auto-ensures an un-bootstrapped user source),tests/brain-mcp-tools.test.ts(Mode B: zero-citation → fallback; citations present → no fallback). Full suite 987 pass, tsc clean. - (This section captured the first two fixes on 2026-06-14; the remaining items — actionable errors, no-silent-success, kb_think ranking — all shipped by 2026-06-15. See the "Shipped" table at the top.)
2026-06-15 update — deeper investigation (live staging brain)#
Followed three more recall complaints to the live staging brain (PGLite snapshot + ArgoCD exec, read-only). This corrected several earlier guesses.
Confirmed via live DB#
- Mode C (scope silo) is real: a
/1on1memoize lands inuser-<id>, which a normal-channel recall (scope =shared+public+kb-*) cannot read. The OKR pages existed in BOTHuser-u0xxxxxxxxx(from the/1on1) andshared(from a later manual re-memoize). The operator had to re-memoize to get them recallable — friction worth removing (default/1on1memoize toshared?). - Mode D (embedding gap) RULED OUT: every chunk is embedded
(
zeroentropyai:zembed-1, 3442/3442,missing=0), vectors are a consistent 1280-dim, and an HNSW cosine index exists. Embeddings are healthy. - Embedding config bug (latent):
values.yamlsetsEMBEDDING_DIMENSIONS=2560but the actual column/vectors/index are 1280 (zembed-1 native). Works by luck (doc + query both 1280); if any path ever honors 2560 for the query, recall breaks. Fix: setEMBEDDING_DIMENSIONS=1280indeploy/agents/example/staging/values.yaml. - The real recall bug is
kb_thinkranking, not storage.gather.tsalready does hybrid (vector + keyword + RRF). A rich (3054-char), well-titled (notes/org-2026-okr→ "Org 2026 OKRs"), embedded, in-scope page still lost the rank race to many BU/dbt OKR pages, and the LLM produced a confident non-empty answer from neighbors — so the zero-citation fallback (above) never fired. Verbose query dilutes; the jot case proved a tightkb_searchkeyword hits rank 1 where full-questionkb_thinkmisses. Levers: query distillation (verbose → keywords) [highest-confidence], title/slug match boost, largergather_limit. Decide retrieval-fix vs synthesis-fix once the recall jsonl confirms gathered-but-ignored vs not-gathered. - Maria misdescribes her own mechanics. She told the operator memoize
"writes a local file, not indexed, needs
/ingest." False —kb_memoize→put_pageupserts the gbrain DB (chunk+embed) directly; the.sources/*.mdwrite-through is an inert byproduct. Worth a soul/skill note so she stops asserting the wrong model.
Storage model (confirmed)#
gbrain DB is the single retrieval source. Boot/nightly syncKbWikis index the
git wiki/ dirs into kb-* sources; runtime memoize writes shared/user-*
sources directly; all recall reads the DB. Nightly sync targets kb-* only, so
it never reconciles/wipes memoized pages, and it reads wiki/, never the
.sources/ write-through mirror.
Follow-ups#
- Durability gap (memoized knowledge has no git backup). Seed KBs survive a
DB wipe (re-sync from git
wiki/). Memoizedshared/user-*pages live ONLY in the PVC gbrain DB — the.sources/*.mdwrite-through is never re-imported by sync (sync readswiki/), so it is not a real backup. If PVC durability is insufficient, add a real backup path: nightly export memoizedshared/user-*pages into a git-backed writable-KBwiki/so sync round-trips them. (This also reframes the "remove write-through" request: removing the mirror loses nothing for recovery — it was already inert.) open_kbremoved (2026-06-15, branchrefactor/remove-open-kb): it was the last KB tool thatreadFileSync'd local markdown at runtime; capability covered by DB-backedkb_list_pages/kb_get_page/kb_search. Every KB tool now sources purely from the gbrain DB.- Memoize DB-only (drop write-through): cosmetic now that the mirror is known
inert; needs gbrain
write_through:false(SHA-pinned dep → fork/patch). EMBEDDING_DIMENSIONS2560 → 1280 in deploy values.
Decisions & current model (2026-06-15)#
After tracing the write/read paths end-to-end, the design intent is settled:
One opinionated write path: kb_memoize → gbrain DB.
kb_memoize(→put_page) upserts the gbrain DB directly (chunk + embed), searchable immediately. The.sources/*.mdwrite-through is an inert mirror — never re-read by sync (sync readswiki/), not a backup.- The pre-gbrain writable-KB path (
slaude_knowledge+/ingest+raw/→wiki→git) is redundant with this. Three ways to "remember" (memoize //ingest/ drop a raw file) is the confusion that misled both the agent and the operator.
slaude_knowledge / /ingest — kept in code, left dormant. Decided NOT to
rip them out for now. Instead: the operator configures all KBs as read-only
(knowledge[]); nothing is set under slaude_knowledge. With no writable KB
configured, kb_memoize → gbrain is the effective sole write path — the "one
opinionated way" achieved operationally, zero code surgery. (Full removal —
/ingest command, ingest.ts, trigger_ingest, ingest-jobs, the config key
— remains an option later if we want to delete the dead path outright.)
/ingest is NOT a step in memoize. It's a separate, manager-gated, git-
backed curation pipeline for a writable KB. Memoize already indexed to the DB;
"ready for /ingest" after a memoize (what Maria said) is wrong. With
slaude_knowledge dormant, /ingest has no target and shouldn't be referenced.
Storage model (confirmed): gbrain DB is the single retrieval source. Boot/
nightly index read-only seed wiki/ dirs → kb-* sources; runtime memoize →
agent/shared/user-* sources; all recall reads the DB. No KB tool reads
markdown at runtime (after open_kb removal).
Durability: PVC-only accepted. Memoized shared/user-* pages live only in
the gbrain DB on the PVC (no git backup; the .sources/ mirror is never
re-imported). A DB snapshot/export is the future option if git-grade durability
is ever needed — not blocking.
Open work (ranked, 2026-06-15) — ALL SHIPPED#
The ranked list below is kept for the record; every item resolved (see "Shipped" table at top for PR refs).
- ✅
kb_thinkranking (Mode B′) — shipped (#38).kb_thinkalways runs a cross-checkkb_searchon a distilled keyword query (verbose NL → keywords, proven by the jot case) and surfaces any strong hit the synthesis didn't cite (search_fallback). Implemented slaude-side (the always-search + distill approach catches both gathered-but-ignored and not-gathered, so the recall-jsonl branch decision was moot). - ✅
open_kbremoval — shipped (#34). - ✅
EMBEDDING_DIMENSIONS2560 → 1280 — shipped (deploy, Bitbucket). - ✅ Standing grant for
put_page— shipped (#36), then hardened (#40) to be per-(thread, writer) after a security review found the thread-only key let one user's approval cover another user's writes in a trusted multi-user thread. - ✅ Mode C — resolved by-design:
/1on1memoize stays personal (user-<id>); team knowledge goes via a shared channel. No re-routing. - ✅ Soul prompt — shipped (#38).
- ✅ #7a/#7b — shipped (#39):
humanizeBrainErroractionable errors; soul baseline forbids papering over a failed brain write. (A gateway stop-guard was considered and rejected —toolResultevents carry no tool name /isError, so the prompt layer is the right seam.)
Design direction — per-source ownership & owner-routed approval (future)#
Today's write gate keys on caller + channel trust, not on the source. The
writable sources a user-context resolves to are only: agent (auto, cron),
user-<id> (auto, private own slice), and shared (approval). kb-* sources
(one per KB in slaude.json) are read-only at runtime — they change only via
their git wiki — and public writes are denied. So "contributing to a KB
source" isn't possible today; shared is the single common writable bucket,
and its approval routes to the global SOUL.md <approvers> (category kb →
manager), not to any per-source owner.
Gaps if we want real per-source governance:
- No
owneron sources. Nothing records who is responsible for a source. - Approval is global, not per-source. A
sharedwrite routes by category, not to "the owner of this source." - Only one shared writable target. No way to stand up a named, contributable
domain/team source distinct from
shared. - Source creation is manager-tier, owner-less (
sources_add).
Proposed model (net-new RBAC, its own design — not part of the memoize fixes):
- Owner metadata per source —
owner:in slaude.json per KB, and anownercolumn on thesourcestable for runtime-created sources. - Named contributable sources — make designated sources user-writable
(beyond the single
shared), each with an owner. - Owner-routed approval — the gate passes the target source's owner as the eligible approver: anyone may contribute, the source's owner approves. The per-thread standing grant (shipped) then still absorbs repeat cards per contributor.
This generalizes the current model: user-<id> is just "a source owned by one
person, auto-approved for that owner"; shared is "a source the manager owns."
Per-source ownership makes that explicit and lets domains/teams own their slice
of the brain. Captured as direction; not scheduled.
Artifacts#
Session JSONL retained locally (gitignored, not committed):
jsonl/thread-1781076888/— Case 1 (Mode A).50555749-…= the FK failure,11abec57-…= a working ingest reference.jsonl/thread-1781143233/— Case 2 write.26ecbaef-…= the successfulkb_put_pageoflessons/jot-deployment-pattern.jsonl/thread-1781168759/— Case 2 retrieval.52b01ab2-…=kb_thinkmiss thenkb_searchhit.