Org-wide Memory — Implementation Plan
Status: Draft v1 · Owner: Engineering · Date: 2026-07-18 Spec: spec.md · Tasks: tasks.md
This plan is additive. Every step adds a controller, a facade method, a plugin category, a table, or a UI surface. Nothing existing is removed, renamed, or refactored. The per-Work KB workbench, the
agent-memorycapability, and thevector-store/content-extractorcategories are untouched. Orgs that never open Memory see no change.
The plan is three phases matching the spec §9 phasing. Each phase is one or more PRs against
develop (NN #21). Every entity/schema change ships its migration in the same PR (NN #16).
Phase P1 — Org-wide aggregation page (search + list over existing KB)
Goal: One org-scoped page that searches and lists everything in the org's KB. No new tables. Reuses EW-651 scope columns.
P1.1 — Repository: relax the KB list guard
- Extend
WorkKnowledgeDocumentRepository.list()(work-knowledge-document.repository.ts) to accept an optionalworkIds: string[]IN-list in addition to the existing singleworkId/organizationIdparams. - Keep the mandatory-scope guard: reject a call with none of
{ workId, organizationId, workIds[] }. The relaxation only adds "all Works in one org", it does not open an unscoped path. - Add an index sanity check: the query filters
workId IN (…)+organizationId— confirm the existing(organizationId)/(workId)indexes cover it; add a composite if the query planner needs it (migration only if an index is added).
P1.2 — Facade: cross-Work semantic search
- Add
VectorStoreFacadeService.queryChunksAcrossWorks(workIds: string[], queryEmbedding, opts)(vector-store-facade.service.ts).pgvector(default/core): singleworkId IN (…)row-filter query.- namespace/collection-per-Work backends (
qdrant): fan out per Work, merge bynormalizedScore. Branch onVectorStoreCapabilities.namespacePerWork. - Preserve the P0 invariant: every underlying chunk query still filters
workId. No query is issued without aworkIdbound.
- Add
KnowledgeBaseService.orgSemanticSearch(orgId, q, filters)that resolves org Work ids (WorkRepository.findIdsByOrganization), embedsqviaAiFacadeService.embed(degrade to lexical-only if no embedding provider), calls the fan-out, and RRF-blends with Postgres FTS — mirroring the existing per-WorksemanticSearch()blend.
P1.3 — API: org-memory.controller.ts
- New
apps/api/src/organizations/org-memory.controller.ts, guard stack mirrored fromorg-kb.controller.ts(OrganizationOwnershipGuard+OrganizationMembershipService.ensureMember/ensureAdmin). - Endpoints: -
GET /api/memory— faceted union feed (KB docs + org docs + uploads + agent-memory read-through). Paginated (cursor). -GET /api/memory/facets— per-facet counts honoring other active filters. -GET /api/memory/stats—{ documentsIndexed, conceptsSynthesized: 0, worksCovered, lastIndexedAt }. -POST /api/memory/documents—+ New; delegates toKnowledgeBaseService.createOrgDocument. -GET /api/memory/documents/:id— uniform item detail (proxies KB doc / agent-memory session). - New service
apps/api/src/organizations/org-memory.service.ts— builds theMemoryItemunion feed, applies facets, callsorgSemanticSearchwhenqpresent, and does the agent-memory read-through (best-effort; empty on failure).
P1.4 — Contracts / DTOs
- Add
MemoryItemDto,MemoryFeedResponseDto,MemoryFacetsDto,MemoryStatsDto,CreateMemoryDocumentDtotopackages/contracts. - OpenAPI annotations on every endpoint (so the MCP server + "chat does everything" pick them up).
P1.5 — Web: proxy + page + sidebar
- Web proxy
apps/web/src/app/api/organizations/[orgId]/memory/**(mirrors the per-Work KB proxy; passesX-Scope-Slug). - New route
apps/web/src/app/[locale]/(dashboard)/organizations/[slug]/memory/page.tsx+ legacy alias. - Page composition (spec §4.2): search input (reuse
KbSearchPalette), header counts, chips (Type/Work/Source/Status/Tag), List view,+ Newdialog (reuse Tiptap editor). Graph toggle present but disabled (P3 tooltip). - Sidebar: insert
{ name: t('navigation.memory'), href: ROUTES.DASHBOARD_MEMORY, icon: Brain }right after the Agents item inDashboardSidebar.tsx(L124). AddROUTES.DASHBOARD_MEMORY, hooksuseOrgMemory()/useMemoryFacets(), and thenavigation.memoryi18n key to all locales.
Tests: repo guard (rejects unscoped, accepts workIds IN-list); facade fan-out preserves
workId filter; controller membership guards; GET /api/memory faceted shape; + New writes an
org-scoped doc; Playwright — open Memory, search, filter by Type/Work, create a note.
Out of scope P1: graph view, cognitive-memory tables, Mission/Team chips, new plugin categories.
Phase P2 — memory + rag plugin categories + first plugin
Goal: Make memory frameworks and multi-doc-type RAG swappable behind a contract, beside the
existing vector-store/content-extractor categories.
P2.1 — Category tuple + contracts
- Append
'memory'and'rag'toPLUGIN_CATEGORIESinplugin-manifest.types.ts(append-only; breadcrumb comment like thevector-store/job-runtimeadditions). - New
packages/plugin/src/contracts/capabilities/memory.interface.ts—IMemoryPlugin(spec §5.2) +MemoryScope,MemoryWriteInput,MemoryRecord,MemoryQuery,MemorySession,PromotionPassInput,SynthesisPassInput. - New
packages/plugin/src/contracts/capabilities/rag.interface.ts—IRagPlugin(spec §5.3) +RagIngestInput,RagQuery,RagHit. - Type guards + optional
base-memory.ts/base-rag.tsabstracts (mirrorbase-vector-store.ts).
P2.2 — Facades
MemoryFacadeService(packages/agent/src/facades/memory-facade.service.ts) — selection cascade (env pin → per-org activeWorkPlugin/org-plugin →defaultForCapabilities→ first-enabled), mirroringVectorStoreFacadeService.MemoryNotConfiguredErrordegrades the agent-memory facet to empty rather than throwing to the page.RagFacadeService— composescontent-extractor+ai-provider.embed+vector-storeforingest; blendedretrievefororgSemanticSearch.
P2.3 — First-party memory plugin (promote agentmemory)
- Update
packages/plugins/agentmemory/manifest to declarecategory: 'memory',capabilities: ['memory', 'agent-memory']— additive: it keeps theagent-memorycapability so the shipped agent-memory pipeline is unbroken, and gains the org-awarememorycapability.defaultForCapabilities: ['memory']. - Implement
IMemoryPluginover the existingagentmemory-client.ts(mapremember/recallontosaveMemory/searchMemory;promote/synthesizeoptional — default to no-op flat tier until P3).
P2.4 — Office-doc extractor (complementary, optional)
- Land
@ever-works/officecli-extractor-pluginas acontent-extractorplugin peroffice-rendering/eval-officecli.md(systemPlugin:false,autoEnable:false, docx/xlsx/pptx text). Run the Alpine/musl gate before wiring it into any image build; fall back tomammoth+SheetJSif musl fails. This feeds more doc types intorag.ingestwith no contract change.
P2.5 — REST surface for capability management
- Extend
apps/api/src/plugins-capabilities/withmemory/+rag/sub-controllers (enable / select active per org), mirroring the existing capability controllers.
Tests: category tuple type guard; MemoryFacadeService selection cascade + degrade;
agentmemory plugin implements both capabilities without breaking the agent-memory pipeline
suite; office extractor canExtract + SSRF-guard reuse.
Out of scope P2: cognitive-memory tables, synthesis, graph, Mission/Team chips.
Phase P3 — Graph + cognitive-memory model + synthesis + Mission/Team facets
Goal: The distilled/graph layer and the two facets that need cross-feature prerequisites.
P3.1 — Cognitive-memory tables (migrations same-PR)
- New entities + migrations (Tier A/C conventions, EW-651):
memory_entry(Tier A),memory_concept(Tier A),memory_concept_link(Tier C denormalized), enumsmemory_partition,memory_tier. Register indatabase.config.tsENTITIESarray (authoritative — noautoLoadEntities; an unregisteredforFeatureentity throwsEntityMetadataNotFoundError→ 500). pgvectorembeddingcolumns; reusekb-chunker.ts+AiFacadeService.embed.
P3.2 — Auto-promotion pass
- Trigger.dev job
memory-promote(spec §6.3): salience increment/decay, threshold-basedsession→work→orgpromotion, embedding-cosine dedupe/merge. Per-org cadence + budget cap (open question §10.5).
P3.3 — Synthesis pass → concepts
- Trigger.dev job
memory-synthesize+POST /api/memory/synthesize(admin-gated): cluster related entries/docs, summarize intomemory_concept, writememory_concept_linkderived-fromedges.conceptsSynthesizedcounter =COUNT(memory_concept WHERE org). Idempotent re-synthesis updates the existing concept.
P3.4 — Graph view
GET /api/memory/graph— nodes (documents/concepts/works) + edges (citations, wiki-links,memory_concept_link), honoring active filters, server-side subgraph windowing (cap node count, top-N by edge weight — open question §10.6).- Web: new graph client component; wire the List|Graph toggle live.
P3.5 — Mission facet (needs Prerequisite A)
- Blocked on a first-class Work→Mission linkage (spec §2.4-A). Recommended: denormalized
nullable
works.missionId(owned by Missions/Ideas/Works feature; additive column + one-time backfill fromacceptedFromIdeaId → WorkProposal.missionId). - Once present: add the
[Mission ▾]chip +mission[]filter +missionId/missionTitleonMemoryItem. Feature-detect so the chip hides if the column is absent.
P3.6 — Team facet (needs Prerequisite B — Teams feature)
- Blocked on
team_resources(teamId, resourceType, resourceId)(spec §2.4-B) — a new additive polymorphic join owned by the Teams feature, NOT this one. Memory only consumes it. - Once present: add the
[Team ▾]chip via reverse lookup (team_resources WHERE resourceType='work' AND resourceId IN (org work ids)), joined into the feed. Feature-detect on the presence ofteam_resources; hide the chip if Teams hasn't shipped.
Tests: entity round-trips + migration idempotency; promotion threshold + dedupe; synthesis idempotency + concept-count; graph node-cap; Mission/Team chips appear only when their prerequisites exist.
Cross-cutting concerns
Prerequisites owned by other features (do not build here)
| Prereq | What | Owner feature | Blocks |
|---|---|---|---|
| A | First-class Work→Mission linkage (denormalized works.missionId recommended) | Missions/Ideas/Works | P3.5 Mission chip |
| B | team_resources(teamId, resourceType, resourceId) polymorphic join | Teams | P3.6 Team chip |
Neither blocks P1 or P2. If either slips, its chip is feature-detected and hidden; the rest of Memory ships.
Database safety
- Forward-only, additive migrations; no DROP / data deletion. New columns nullable on insert.
- Every P3 entity registered in the
ENTITIESarray indatabase.config.tsin the same PR (authoritative registration; unregisteredforFeature→ runtime 500). - Pre-checks (fail-loud) on any backfill.
Invariants to preserve
- P0 chunk
workIdfilter — cross-Work search fans out per Work; never an unscoped chunk query. - KB list mandatory-scope guard — relaxation adds a Work-ids IN-list, still rejects unscoped.
- Agent-memory best-effort — external backend failure degrades the facet, never the page.
- Untrusted memory output — fenced by consumers before any prompt injection.
Sequencing summary
| Phase | Depends on | PR target |
|---|---|---|
| P1 | EW-651 scope columns (shipped) | develop |
| P2 | P1 | develop |
| P3.1–P3.4 | P2 | develop |
| P3.5 (Mission chip) | Prereq A | develop |
| P3.6 (Team chip) | Prereq B (Teams feature) | develop |