From d321b7667cc0bab3c9eccbfc227a2eeaee8a4f3c Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 13 Aug 2026 20:45:46 -0700 Subject: [PATCH] Rename knowledge schema to memory; config reads DATABASE_URL (CL-6009) Pre-GA safe rename: no deployment has run migrations against the `knowledge` schema yet, so this is a clean cutover with no back-compat concerns. Postgres schema is now `memory`, table/type identifiers (memoryDocument, MemoryEdgeHint, etc) follow suit, and loadMemoryConfig reads DATABASE_URL instead of KNOWLEDGE_DATABASE_URL. Baseline migration edited in place and renamed to 0002_memory_baseline.sql since it has never run in production. --- .env.example | 8 +- AGENTS.md | 12 +-- ARCHITECTURE.md | 2 +- CHANGELOG.md | 10 +- IMPLEMENTATION.md | 82 +++++++------- PRODUCT.md | 4 +- README.md | 6 +- compose.yml | 16 +-- docs/AUTHZ-DOCUMENT-ACCESS.md | 2 +- migrations/0001_extensions.sql | 4 +- ..._baseline.sql => 0002_memory_baseline.sql} | 58 +++++----- scripts/db-setup.ts | 4 +- src/config.ts | 6 +- src/core/adapt-and-plan.ts | 4 +- src/core/embed-client.ts | 2 +- src/core/embed-model-registry.test.ts | 10 +- src/core/embed-model-registry.ts | 14 +-- src/core/embed-worker.ts | 2 +- src/core/engine-client-config.ts | 4 +- src/core/fts-language.test.ts | 2 +- src/core/fts-language.ts | 20 ++-- src/core/generation.ts | 2 +- src/core/schemas/adapted-document.ts | 6 +- src/core/schemas/chunk.test.ts | 10 +- src/core/schemas/chunk.ts | 4 +- src/core/schemas/document.test.ts | 22 ++-- src/core/schemas/document.ts | 14 +-- src/core/schemas/entity-edge.test.ts | 28 ++--- src/core/schemas/entity-edge.ts | 36 +++---- src/core/schemas/search.ts | 4 +- src/db/schema.test.ts | 60 +++++------ src/db/schema.ts | 30 +++--- src/log.ts | 2 +- src/memory.ts | 6 +- src/migrations.ts | 12 +-- src/mount-config.test.ts | 2 +- src/mount-config.ts | 4 +- src/services/capture.ts | 86 +++++++-------- src/services/search.test.ts | 8 +- src/services/search.ts | 102 +++++++++--------- src/services/timeline.ts | 28 ++--- src/services/transform.ts | 2 +- 42 files changed, 370 insertions(+), 370 deletions(-) rename migrations/{0002_knowledge_baseline.sql => 0002_memory_baseline.sql} (73%) diff --git a/.env.example b/.env.example index caf0612..4aa4870 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,9 @@ -# ── Knowledge engine SDK config (loadKnowledgeConfig) ─────────────────────── +# ── Memory engine SDK config (loadMemoryConfig) ───────────────────────────── # The SDK mounts onto your Interchange app; the host owns auth, tenancy, and -# grants. These vars configure only the knowledge/vector plane. +# grants. These vars configure only the memory/vector plane. -# Knowledge / vector Postgres (compose.yml → localhost:5434). -KNOWLEDGE_DATABASE_URL=postgres://knowledge:knowledge-dev-password@localhost:5434/knowledge +# Memory / vector Postgres (compose.yml → localhost:5434). +DATABASE_URL=postgres://memory:memory-dev-password@localhost:5434/memory DB_POOL_MAX=8 # Embeddings (compose.yml → Ollama on :11434). The engine never embeds diff --git a/AGENTS.md b/AGENTS.md index 3c08490..35e3107 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ port, or process entrypoint here, and there never should be. bun install # Bun 1.2+ required bun run typecheck # tsc --noEmit bun run test # bun test ./src (no network, no Postgres) -bun run db:setup # apply migrations (needs KNOWLEDGE_DATABASE_URL) +bun run db:setup # apply migrations (needs DATABASE_URL) docker compose up -d # local pgvector + model endpoints for manual runs ``` @@ -28,7 +28,7 @@ CI runs `typecheck` + `test` — both must pass before any push. - `src/services/` — capture / search / transform internals (not public verbs) - `src/ports/` — `DocumentStore` / `SourceProvider` + fakes - `src/core/` — embed/rerank clients, merge, arktype schemas -- `src/db/` + `migrations/` — Drizzle schema + SQL migrations (pgvector, `knowledge.*`) +- `src/db/` + `migrations/` — Drizzle schema + SQL migrations (pgvector, `memory.*`) - `packages/` — removed; DocumentStore adapters and Linear tools are sibling packages (`@corbits/mem0-memory-adapter`, `@corbits/supermemory-memory-adapter`, `@corbits/linear-tools`). @@ -38,10 +38,10 @@ CI runs `typecheck` + `test` — both must pass before any push. 1. **Authenticate nothing.** Identity is `c.get("principal")` from the Interchange context; authorization goes through the host's grant store (`@intx/authz`). Never add API keys, sessions, or OAuth here. -2. **One Postgres**: `KNOWLEDGE_DATABASE_URL`, the engine's own vector plane. - There is deliberately no `DATABASE_URL` fallback — the host's control-plane - DB is never ours. No foreign keys into control-plane tables; cross-refs - (`tenant_id`, `principal_id`) are plain `text`. +2. **One Postgres**: `DATABASE_URL`, the engine's own vector plane, under the + `memory` schema — never the host's control-plane DB. No foreign keys into + control-plane tables; cross-refs (`tenant_id`, `principal_id`) are plain + `text`. 3. **Never embed in-process.** Embedding/reranking are outbound HTTP calls to configured endpoints. A model endpoint is a trusted URL, same as the database URL — no self-host flags, no SSRF filtering here. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index de59243..d56cbac 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -33,7 +33,7 @@ config (or an injected store). ## Boundaries - **Runtime**: Bun + Hono, mounted on the host app. **DB**: own pgvector - Postgres (`KNOWLEDGE_DATABASE_URL`) unless `documentStore` is injected. + Postgres (`DATABASE_URL`) unless `documentStore` is injected. **Types**: arktype at every route boundary. - **No auth of its own.** Interchange resolves the caller and puts `principal` + `tenant` on context; routes read identity from there diff --git a/CHANGELOG.md b/CHANGELOG.md index 19f99b5..8006dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `MemoryConfig`, `MemoryError`. HTTP paths are under `/api/tenants/:tenantId/memory/`; grants are `memory:add` / `memory:search`; access tags use `memory.owner:` / `memory.tenant:` - / `memory.space:`. Postgres schema name remains `knowledge`. + / `memory.space:`. Postgres schema is `memory`. +- **Breaking:** config reads `DATABASE_URL` (was `KNOWLEDGE_DATABASE_URL`) for + the engine's own pgvector Postgres connection. - **Breaking:** memory plane surface is `add` / `search` / `list` with `principalId` + `tenantId` only. Inference is host-owned (no answer endpoint or personal-memory side-channel on the plane). @@ -42,8 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 creator-always + host `GrantStore`), not the visibility-mode / block-list mini-ACL. Share sugar only mints tags. See `docs/AUTHZ-DOCUMENT-ACCESS.md`. - **Breaking:** Postgres baseline is two files (`0001_extensions` + - `0002_knowledge_baseline`) with `access_tags` and no `visibility_*` columns. - Fresh installs only — drop/recreate the knowledge schema on existing DBs. + `0002_memory_baseline`) with `access_tags` and no `visibility_*` columns. + Fresh installs only — drop/recreate the memory schema on existing DBs. - **Breaking:** `grantStore` + `conditionRegistry` are top-level `createMemory` options (no nested `grants: { … }`). @@ -51,7 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Optional `TextExtractor` + `file` XOR `content` on `add` - `share` sugar on `add` (maps to access tags only: owner, tenant, peers) -- `access_tags` on `knowledge.document` (baseline schema; Postgres schema name unchanged) +- `access_tags` on `memory.document` (baseline schema) ### Removed diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 8e489d2..49143a3 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -24,7 +24,7 @@ src/ deps.ts # RouteDeps, caller(c) (context identity), grantGuard add.ts, search.ts, list.ts db/ - schema.ts # Drizzle table defs (knowledge.* schema) + schema.ts # Drizzle table defs (memory.* schema) client.ts # createDb(config) -> { db (drizzle), sql (raw postgres-js) } services/ capture.ts # captureDocument, deriveFromRawCapture — the write path @@ -51,7 +51,7 @@ singletons except the logger). Nothing has import-time side effects, so unit tests exercise the routes and services directly without a listening server. Mounting does **not** verify the FTS language against the database at boot — -see the `knowledge_chunk` section below. A host is expected to either run +see the `memory_chunk` section below. A host is expected to either run `runMemoryMigrations` itself (which verifies) or wire its own readiness probe to call `verifyFtsLanguage`; without one of those, a language mismatch surfaces as a runtime failure on the plane's first query, not at mount time. @@ -75,7 +75,7 @@ fallback)` parses a positive integer or throws. | Var | Required? | Default | Notes | |---|---|---|---| -| `KNOWLEDGE_DATABASE_URL` | **yes** | — | the engine's own pgvector Postgres | +| `DATABASE_URL` | **yes** | — | the engine's own pgvector Postgres | | `DB_POOL_MAX` | no | `8` | postgres-js pool size | | `FTS_LANGUAGE` | no | `english` | text search config for the lexical channel; fixed into the generated column at migration time — changing it later requires rebuilding the column (recipe below), and `runMemoryMigrations` fails loudly if config and column disagree. Unqualified `pg_catalog` config names only — a schema-qualified config (`myschema.mycfg`) is rejected explicitly, both when configuring and when read back from an already-migrated column. | | `EMBED_BASE_URL` | **yes** | — | embed endpoint root, no path suffix | @@ -99,7 +99,7 @@ embed/rerank endpoint the engine calls — its own `EngineConfig.embed`/ the embed-model-registry probe/activation, and `toRerankClientConfig`) and a `transform_config`'s replay embed override (`buildEmbedClientConfig` in `services/transform.ts`) — is treated as a trusted URL, exactly like -`KNOWLEDGE_DATABASE_URL`. There is no private-IP / SSRF filtering and no `allowSelfHost` +`DATABASE_URL`. There is no private-IP / SSRF filtering and no `allowSelfHost` knob anywhere: a self-hosted endpoint on `localhost` or a private IP is just a URL, indistinguishable from a managed provider. Model/replay endpoints are configured by the operator (env) or named by a trusted caller in a @@ -110,10 +110,10 @@ egress control front the endpoints with an allowlisting proxy. All tables are Drizzle-defined in `db/schema.ts`, DDL'd in `migrations/` (applied by `scripts/db-setup.ts`, tracked in a `_migrations` ledger table so -re-running is a no-op). No knowledge table has a foreign key into any +re-running is a no-op). No memory table has a foreign key into any control-plane table — `tenant_id`/`principal_id`/source refs are plain `text`. -### `knowledge_document` +### `memory_document` The stable logical row for a captured source. Unique on `(tenant_id, adapter, external_ref)` — this triple is the dedupe/identity key every capture upserts against. Document access is **grant tags**: @@ -121,10 +121,10 @@ every capture upserts against. Document access is **grant tags**: `docs/AUTHZ-DOCUMENT-ACCESS.md`). `attributes` is a flat jsonb bag of scalars. `last_seen_at` bumps on every re-capture, even a content-hash NOOP. -### `knowledge_version` +### `memory_version` The versioned body of a document. `version` is a monotonic integer scoped to `(document_id, generation)` — **not** globally per-document — per the -`knowledge_version_document_generation_version_uniq` unique index (baseline +`memory_version_document_generation_version_uniq` unique index (baseline schema). `status` tracks `'active'|'superseded'|'deprecated'|'archived'|'tombstoned'`; only one `active` row exists per `(document_id, generation)` at a time (the capture path enforces this by flipping the prior active row to `superseded` @@ -140,7 +140,7 @@ replay-generation tag: the normal add path always writes so a replayed corpus's versions never collide with, or even become visible alongside, the live ones unless a caller explicitly searches that generation. -### `knowledge_chunk` +### `memory_chunk` An ordered slice of a version's text, keyed by `(version_id, ordinal)` (unique). Carries a generated-always `text_fts tsvector` column (GIN-indexed) that powers the lexical search channel — this is the only place FTS is @@ -171,36 +171,36 @@ alter an existing one. One-time recipe (verified against a live ```sql BEGIN; -DROP INDEX IF EXISTS knowledge_chunk_text_fts_idx; -ALTER TABLE knowledge_chunk DROP COLUMN text_fts; -ALTER TABLE knowledge_chunk ADD COLUMN text_fts tsvector +DROP INDEX IF EXISTS memory_chunk_text_fts_idx; +ALTER TABLE memory_chunk DROP COLUMN text_fts; +ALTER TABLE memory_chunk ADD COLUMN text_fts tsvector GENERATED ALWAYS AS (to_tsvector('', "text")) STORED; COMMIT; -- Separate statement/connection — CREATE INDEX CONCURRENTLY is rejected -- inside any transaction block, unconditionally, since Postgres 8.2. It -- cannot be combined with the BEGIN/COMMIT block above. -CREATE INDEX CONCURRENTLY knowledge_chunk_text_fts_idx ON knowledge_chunk USING gin (text_fts); +CREATE INDEX CONCURRENTLY memory_chunk_text_fts_idx ON memory_chunk USING gin (text_fts); ``` Both `ALTER TABLE` statements take an `ACCESS EXCLUSIVE` lock and force a full table rewrite (dropping then re-adding a `STORED` generated column -always rewrites) — plan for a stall on `knowledge_chunk` for the duration on +always rewrites) — plan for a stall on `memory_chunk` for the duration on a populated database; run in a maintenance window. Only unqualified `pg_catalog` config names are supported; a schema-qualified config on this column is rejected explicitly by `verifyFtsLanguage` (with this same recipe in the error) rather than silently mis-parsed. -### `knowledge_entity` / `knowledge_edge` -Lightweight graph rows. `knowledge_entity` has no unique constraint; dedupe on +### `memory_entity` / `memory_edge` +Lightweight graph rows. `memory_entity` has no unique constraint; dedupe on `(tenant_id, kind, identifiers)` is done in application code (`upsertEntity` in `capture.ts`, an exact-match linear scan per kind). Same for -`knowledge_edge` (dedupe on the full `(tenant_id, rel, from, to)` tuple, +`memory_edge` (dedupe on the full `(tenant_id, rel, from, to)` tuple, `upsertEdge`). `rel` is constrained (DB CHECK + arktype) to `'about'|'produced_by'|'links'|'parent'|'mentions'|'waiting_on'`; `from_type`/ `to_type` to `'document'|'entity'|'native'`. -### `knowledge_embed_model` +### `memory_embed_model` Per-tenant registry of which embed model is currently active, and the dimensionality it was discovered at (`discoverModelDims`/`probeEmbedDims` — dims are **never** hard-coded, always probed live against the endpoint). @@ -210,18 +210,18 @@ most-recently-`updated_at` row with `status = 'active'` for that tenant (`resolveActiveEmbedTable`) — there is no per-generation embed-model scoping (see "Known limitation" under Raw + replay below). -### Dynamic per-model vector tables: `knowledge_embedding_` +### Dynamic per-model vector tables: `memory_embedding_` Not in `db/schema.ts` (no fixed shape — dimensionality varies by model) and not in any migration file. Created at runtime by `activateEmbedModel` (`embed-model-registry.ts`) the first time a given `(baseUrl, modelId)` pair is used: ```sql -CREATE TABLE IF NOT EXISTS knowledge_embedding_ ( +CREATE TABLE IF NOT EXISTS memory_embedding_ ( chunk_id text PRIMARY KEY, tenant_id text NOT NULL, embedding vector(), - CONSTRAINT knowledge_embedding__chunk_fk - FOREIGN KEY (chunk_id) REFERENCES knowledge_chunk (id) ON DELETE CASCADE + CONSTRAINT memory_embedding__chunk_fk + FOREIGN KEY (chunk_id) REFERENCES memory_chunk (id) ON DELETE CASCADE ) ``` plus a `(tenant_id, chunk_id)` index (created on every activation, so it @@ -231,9 +231,9 @@ EXISTS` cannot add the FK to a table created before it existed; that gap is accepted (no populated pre-FK installs exist). If hard deletes are ever introduced against such a table, run once, per embedding table: ```sql -ALTER TABLE knowledge_embedding_ - ADD CONSTRAINT knowledge_embedding__chunk_fk - FOREIGN KEY (chunk_id) REFERENCES knowledge_chunk (id) ON DELETE CASCADE; +ALTER TABLE memory_embedding_ + ADD CONSTRAINT memory_embedding__chunk_fk + FOREIGN KEY (chunk_id) REFERENCES memory_chunk (id) ON DELETE CASCADE; ``` plus an HNSW index: `vector_cosine_ops` up to 2000 dims (falling back to `ivfflat` if the Postgres/pgvector build lacks the `hnsw` access method), or @@ -248,8 +248,8 @@ at activation instead. The dense query's `ORDER BY` is generated by `cosineDistanceExpr` from the same module so it always matches the indexed expression. The table name is validated against `EMBED_TABLE_NAME_PATTERN` -(`/^knowledge_embedding_[a-f0-9]{16}$/`) both when computed and again every -time it's read back from `knowledge_embed_model`, before ever being +(`/^memory_embedding_[a-f0-9]{16}$/`) both when computed and again every +time it's read back from `memory_embed_model`, before ever being string-interpolated into raw SQL — this is the only place in the codebase a computed identifier is spliced into DDL/DML. @@ -301,7 +301,7 @@ returns the run summary either way. a new one, all inside the same transaction as the derived rows. 3. **`deriveVersionInTransaction`** — the single derivation core shared by live capture and replay: - - No existing `knowledge_document` for `(tenantId, adapter, externalRef)` + - No existing `memory_document` for `(tenantId, adapter, externalRef)` → insert a new document row + a version at `version = 1`, `supersedesVersionId = null`. - Existing document, and its current `active` version (scoped to this @@ -365,11 +365,11 @@ search); otherwise it throws `MemorySearchInputError` (400). 2. **Lexical channel** — `fetchLexicalCandidates`: Postgres full-text search (`ts_rank` against `plainto_tsquery` in the configured `FTS_LANGUAGE`, bound as a `regconfig` parameter, over - `knowledge_chunk.text_fts`), joined to `knowledge_version` (filtered to - `status = 'active'` and the resolved `generation`) and `knowledge_document` + `memory_chunk.text_fts`), joined to `memory_version` (filtered to + `status = 'active'` and the resolved `generation`) and `memory_document` (tenant-scoped only — document access is grant-tag post-filter in the plane), optionally further filtered by - `kinds` and/or `entityIds` (via a sub-select against `knowledge_edge`). + `kinds` and/or `entityIds` (via a sub-select against `memory_edge`). Overfetches up to `overfetchLimit` rows, non-deduped, per-chunk. 3. **Dense channel** — `fetchDenseCandidates`: embeds the query (`embedTexts`), resolves the tenant's single active embedding table @@ -379,7 +379,7 @@ search); otherwise it throws `MemorySearchInputError` (400). `(e.embedding::halfvec(N)) <=> $vector::halfvec(N)` expression above that so the halfvec HNSW index is used) against that table joined back to - `knowledge_chunk`/`knowledge_version`/`knowledge_document` with the + `memory_chunk`/`memory_version`/`memory_document` with the **same tenant-only scope** as the lexical channel (no mini-ACL in SQL). Returns `null` (not an error) when there's no active embed model yet or the query is empty; a thrown error from the embed call or @@ -511,11 +511,11 @@ timeline maps different columns: | Wire field | Source column / meaning | |---|---| -| `at` | `knowledge_document.last_seen_at` (ISO) — re-captures rise in the feed | -| `title` | `knowledge_document.title` | -| `source` | `knowledge_document.adapter` (HTTP add defaults to `"http"`, not `"api"`) | -| `tenantId` | `knowledge_document.tenant_id` | -| `principalId` | `knowledge_version.created_by_principal_id` of the active live version (empty string when null) — the capturing actor stored on the version, not the request principal of a later timeline read | +| `at` | `memory_document.last_seen_at` (ISO) — re-captures rise in the feed | +| `title` | `memory_document.title` | +| `source` | `memory_document.adapter` (HTTP add defaults to `"http"`, not `"api"`) | +| `tenantId` | `memory_document.tenant_id` | +| `principalId` | `memory_version.created_by_principal_id` of the active live version (empty string when null) — the capturing actor stored on the version, not the request principal of a later timeline read | ### Document access (grant tags) @@ -523,7 +523,7 @@ Document access is Interchange authz — **not** a mini-ACL. - Write path: `resolveAccessTags` always writes `memory.owner:` and merges optional `accessTags` / share sugar (`tenant`, peer `principals`, - explicit `tags`). Stored on `knowledge.document.access_tags`. + explicit `tags`). Stored on `memory.document.access_tags`. - Read path (search + list): `canAccessDocument` — creator always allowed; otherwise `authorize(grantStore, principal, tenant, tag, "search")` for any tag on the document. @@ -549,15 +549,15 @@ docker compose up -d # pgvector + Ollama + rer docker compose exec ollama ollama pull nomic-embed-text cp .env.example .env bun install -bun run db:setup # apply the knowledge schema, idempotent +bun run db:setup # apply the memory schema, idempotent bun run test # unit suite (no external services) ``` -`compose.yml` provisions the pgvector Postgres (`knowledge` db, host port +`compose.yml` provisions the pgvector Postgres (`memory` db, host port `5434`), an Ollama embeddings server (`:11434`), and a TEI reranker (`:8085`). The engine **never embeds internally** — `EMBED_BASE_URL` must point at a real endpoint. A model endpoint is just a URL + capability options, trusted the same -as `KNOWLEDGE_DATABASE_URL`: +as `DATABASE_URL`: - **Local default**: Ollama at `http://localhost:11434` (`EMBED_API_STYLE=ollama`, `EMBED_MODEL=nomic-embed-text`). diff --git a/PRODUCT.md b/PRODUCT.md index e4e070d..94dee05 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -77,8 +77,8 @@ stores, Linear tools. Core never imports vendor SDKs. - No answer/generation endpoint — host owns inference. - Workbench is a client, not required. -**Default durable store:** Postgres via `KNOWLEDGE_DATABASE_URL` only (no -`DATABASE_URL` fallback), tables under the **`knowledge`** schema. When +**Default durable store:** Postgres via `DATABASE_URL`, tables under the +**`memory`** schema. When `documentStore` is injected, Postgres is not opened. Cross-refs are plain `text` — no FKs into the host control plane. diff --git a/README.md b/README.md index 4bab4c6..55b9aaa 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ import { createMemory, loadMemoryConfig } from "@corbits/memory"; const memory = createMemory({ app, - config: loadMemoryConfig(), // KNOWLEDGE_DATABASE_URL + embed env + config: loadMemoryConfig(), // DATABASE_URL + embed env grantStore, conditionRegistry, }); @@ -124,11 +124,11 @@ always sees their own docs. Details: ## Config `loadMemoryConfig()` reads env (see `.env.example`). For the default pgvector -store you need `KNOWLEDGE_DATABASE_URL`, `EMBED_BASE_URL`, `EMBED_MODEL`. +store you need `DATABASE_URL`, `EMBED_BASE_URL`, `EMBED_MODEL`. ```ts import { runMemoryMigrations } from "@corbits/memory/migrations"; -await runMemoryMigrations(process.env.KNOWLEDGE_DATABASE_URL!); +await runMemoryMigrations(process.env.DATABASE_URL!); ``` Inject `documentStore` to use fakes, a host store, or a sibling adapter instead diff --git a/compose.yml b/compose.yml index 466a359..387cedf 100644 --- a/compose.yml +++ b/compose.yml @@ -1,4 +1,4 @@ -# Local dev dependencies for the knowledge engine SDK. +# Local dev dependencies for the memory engine SDK. # # docker compose up -d # DB + embeddings + reranker # docker compose exec ollama ollama pull nomic-embed-text @@ -8,19 +8,19 @@ # compose only stands up the backing services the engine talks to. services: - # Knowledge / vector store (pgvector). + # Memory / vector store (pgvector). postgres: image: pgvector/pgvector:pg17 ports: - "5434:5432" environment: - POSTGRES_USER: knowledge - POSTGRES_PASSWORD: knowledge-dev-password - POSTGRES_DB: knowledge + POSTGRES_USER: memory + POSTGRES_PASSWORD: memory-dev-password + POSTGRES_DB: memory volumes: - - knowledge-data:/var/lib/postgresql/data + - memory-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U knowledge -d knowledge"] + test: ["CMD-SHELL", "pg_isready -U memory -d memory"] interval: 5s timeout: 5s retries: 5 @@ -44,6 +44,6 @@ services: - reranker-cache:/data volumes: - knowledge-data: + memory-data: ollama-models: reranker-cache: diff --git a/docs/AUTHZ-DOCUMENT-ACCESS.md b/docs/AUTHZ-DOCUMENT-ACCESS.md index af1a31e..6f1ee3e 100644 --- a/docs/AUTHZ-DOCUMENT-ACCESS.md +++ b/docs/AUTHZ-DOCUMENT-ACCESS.md @@ -161,7 +161,7 @@ There is no `visibility_mode`, principal-id array, block list, or dual-write ACL column. Share sugar only mints tags via `resolveAccessTags`. Fresh databases apply the baseline migrations (`0001_extensions.sql` + -`0002_knowledge_baseline.sql`) with `access_tags` from day one. +`0002_memory_baseline.sql`) with `access_tags` from day one. ## Non-goals diff --git a/migrations/0001_extensions.sql b/migrations/0001_extensions.sql index c54e2c2..c2135e0 100644 --- a/migrations/0001_extensions.sql +++ b/migrations/0001_extensions.sql @@ -1,5 +1,5 @@ --- Knowledge plane owns its own Postgres schema. Never pollutes public. -CREATE SCHEMA IF NOT EXISTS "knowledge"; +-- Memory plane owns its own Postgres schema. Never pollutes public. +CREATE SCHEMA IF NOT EXISTS "memory"; -- pgvector for dense embeddings (cosine distance / HNSW). CREATE EXTENSION IF NOT EXISTS vector; diff --git a/migrations/0002_knowledge_baseline.sql b/migrations/0002_memory_baseline.sql similarity index 73% rename from migrations/0002_knowledge_baseline.sql rename to migrations/0002_memory_baseline.sql index a9bc328..60bf66b 100644 --- a/migrations/0002_knowledge_baseline.sql +++ b/migrations/0002_memory_baseline.sql @@ -1,7 +1,7 @@ --- Single baseline for the knowledge plane (grant-tag authz from day one). +-- Single baseline for the memory plane (grant-tag authz from day one). -- Document access is access_tags + creator post-filter; no visibility_* columns. -CREATE TABLE IF NOT EXISTS "knowledge"."document" ( +CREATE TABLE IF NOT EXISTS "memory"."document" ( "id" text PRIMARY KEY, "tenant_id" text NOT NULL, "kind" text NOT NULL, @@ -15,9 +15,9 @@ CREATE TABLE IF NOT EXISTS "knowledge"."document" ( ); CREATE UNIQUE INDEX IF NOT EXISTS "document_tenant_adapter_external_ref_uniq" - ON "knowledge"."document" ("tenant_id", "adapter", "external_ref"); + ON "memory"."document" ("tenant_id", "adapter", "external_ref"); -CREATE TABLE IF NOT EXISTS "knowledge"."raw_capture" ( +CREATE TABLE IF NOT EXISTS "memory"."raw_capture" ( "id" text PRIMARY KEY, "tenant_id" text NOT NULL, "adapter" text NOT NULL, @@ -31,15 +31,15 @@ CREATE TABLE IF NOT EXISTS "knowledge"."raw_capture" ( ); CREATE UNIQUE INDEX IF NOT EXISTS "raw_capture_tenant_source_hash_uniq" - ON "knowledge"."raw_capture" ("tenant_id", "source_hash"); + ON "memory"."raw_capture" ("tenant_id", "source_hash"); CREATE INDEX IF NOT EXISTS "raw_capture_tenant_adapter_external_ref_idx" - ON "knowledge"."raw_capture" ("tenant_id", "adapter", "external_ref"); + ON "memory"."raw_capture" ("tenant_id", "adapter", "external_ref"); -CREATE TABLE IF NOT EXISTS "knowledge"."version" ( +CREATE TABLE IF NOT EXISTS "memory"."version" ( "id" text PRIMARY KEY, "tenant_id" text NOT NULL, - "document_id" text NOT NULL REFERENCES "knowledge"."document" ("id") ON DELETE CASCADE, + "document_id" text NOT NULL REFERENCES "memory"."document" ("id") ON DELETE CASCADE, "version" integer NOT NULL, "supersedes_version_id" text, "status" text NOT NULL DEFAULT 'active', @@ -55,7 +55,7 @@ CREATE TABLE IF NOT EXISTS "knowledge"."version" ( "actor_count" integer NOT NULL DEFAULT 1, "has_social_signal" boolean NOT NULL DEFAULT false, "source_class" text NOT NULL DEFAULT 'native', - "raw_capture_id" text REFERENCES "knowledge"."raw_capture" ("id"), + "raw_capture_id" text REFERENCES "memory"."raw_capture" ("id"), "generation" text NOT NULL DEFAULT 'live', CONSTRAINT "version_status_check" CHECK ("status" IN ('active', 'superseded', 'deprecated', 'archived', 'tombstoned')), @@ -66,16 +66,16 @@ CREATE TABLE IF NOT EXISTS "knowledge"."version" ( ); CREATE UNIQUE INDEX IF NOT EXISTS "version_document_generation_version_uniq" - ON "knowledge"."version" ("document_id", "generation", "version"); + ON "memory"."version" ("document_id", "generation", "version"); CREATE INDEX IF NOT EXISTS "version_document_status_idx" - ON "knowledge"."version" ("document_id", "status"); + ON "memory"."version" ("document_id", "status"); -CREATE TABLE IF NOT EXISTS "knowledge"."chunk" ( +CREATE TABLE IF NOT EXISTS "memory"."chunk" ( "id" text PRIMARY KEY, "tenant_id" text NOT NULL, - "version_id" text NOT NULL REFERENCES "knowledge"."version" ("id") ON DELETE CASCADE, - "document_id" text NOT NULL REFERENCES "knowledge"."document" ("id") ON DELETE CASCADE, + "version_id" text NOT NULL REFERENCES "memory"."version" ("id") ON DELETE CASCADE, + "document_id" text NOT NULL REFERENCES "memory"."document" ("id") ON DELETE CASCADE, "ordinal" integer NOT NULL, "text" text NOT NULL, "role" text, @@ -83,18 +83,18 @@ CREATE TABLE IF NOT EXISTS "knowledge"."chunk" ( ); CREATE UNIQUE INDEX IF NOT EXISTS "chunk_version_ordinal_uniq" - ON "knowledge"."chunk" ("version_id", "ordinal"); + ON "memory"."chunk" ("version_id", "ordinal"); --- {{FTS_LANGUAGE}} is substituted by runKnowledgeMigrations from FTS_LANGUAGE +-- {{FTS_LANGUAGE}} is substituted by runMemoryMigrations from FTS_LANGUAGE -- (or opts.ftsLanguage). Must match the language used at query time. -ALTER TABLE "knowledge"."chunk" +ALTER TABLE "memory"."chunk" ADD COLUMN IF NOT EXISTS "text_fts" tsvector GENERATED ALWAYS AS (to_tsvector('{{FTS_LANGUAGE}}', "text")) STORED; CREATE INDEX IF NOT EXISTS "chunk_text_fts_idx" - ON "knowledge"."chunk" USING GIN ("text_fts"); + ON "memory"."chunk" USING GIN ("text_fts"); -CREATE TABLE IF NOT EXISTS "knowledge"."entity" ( +CREATE TABLE IF NOT EXISTS "memory"."entity" ( "id" text PRIMARY KEY, "tenant_id" text NOT NULL, "kind" text NOT NULL, @@ -104,9 +104,9 @@ CREATE TABLE IF NOT EXISTS "knowledge"."entity" ( ); CREATE INDEX IF NOT EXISTS "entity_tenant_kind_idx" - ON "knowledge"."entity" ("tenant_id", "kind"); + ON "memory"."entity" ("tenant_id", "kind"); -CREATE TABLE IF NOT EXISTS "knowledge"."edge" ( +CREATE TABLE IF NOT EXISTS "memory"."edge" ( "id" text PRIMARY KEY, "tenant_id" text NOT NULL, "rel" text NOT NULL, @@ -127,12 +127,12 @@ CREATE TABLE IF NOT EXISTS "knowledge"."edge" ( ); CREATE INDEX IF NOT EXISTS "edge_from_idx" - ON "knowledge"."edge" ("tenant_id", "from_type", "from_ref"); + ON "memory"."edge" ("tenant_id", "from_type", "from_ref"); CREATE INDEX IF NOT EXISTS "edge_to_idx" - ON "knowledge"."edge" ("tenant_id", "to_type", "to_ref"); + ON "memory"."edge" ("tenant_id", "to_type", "to_ref"); -CREATE TABLE IF NOT EXISTS "knowledge"."embed_model" ( +CREATE TABLE IF NOT EXISTS "memory"."embed_model" ( "id" text PRIMARY KEY, "tenant_id" text NOT NULL, "model_key" text NOT NULL, @@ -145,7 +145,7 @@ CREATE TABLE IF NOT EXISTS "knowledge"."embed_model" ( CHECK ("status" IN ('active', 'retired')) ); -CREATE TABLE IF NOT EXISTS "knowledge"."transform_config" ( +CREATE TABLE IF NOT EXISTS "memory"."transform_config" ( "id" text PRIMARY KEY, "tenant_id" text NOT NULL, "name" text NOT NULL, @@ -156,10 +156,10 @@ CREATE TABLE IF NOT EXISTS "knowledge"."transform_config" ( UNIQUE ("tenant_id", "name", "version") ); -CREATE TABLE IF NOT EXISTS "knowledge"."transform_run" ( +CREATE TABLE IF NOT EXISTS "memory"."transform_run" ( "id" text PRIMARY KEY, "tenant_id" text NOT NULL, - "config_id" text NOT NULL REFERENCES "knowledge"."transform_config" ("id"), + "config_id" text NOT NULL REFERENCES "memory"."transform_config" ("id"), "scope" jsonb NOT NULL DEFAULT '{}', "generation" text NOT NULL, "status" text NOT NULL DEFAULT 'running', @@ -173,7 +173,7 @@ CREATE TABLE IF NOT EXISTS "knowledge"."transform_run" ( ); CREATE UNIQUE INDEX IF NOT EXISTS "transform_run_generation_uniq" - ON "knowledge"."transform_run" ("generation"); + ON "memory"."transform_run" ("generation"); CREATE INDEX IF NOT EXISTS "transform_run_tenant_config_idx" - ON "knowledge"."transform_run" ("tenant_id", "config_id"); + ON "memory"."transform_run" ("tenant_id", "config_id"); diff --git a/scripts/db-setup.ts b/scripts/db-setup.ts index 0e2487e..7c3fc70 100644 --- a/scripts/db-setup.ts +++ b/scripts/db-setup.ts @@ -1,8 +1,8 @@ import { runMemoryMigrations } from "../src/migrations.ts"; const url = - process.env["KNOWLEDGE_DATABASE_URL"]; -if (!url) throw new Error("KNOWLEDGE_DATABASE_URL is required"); + process.env["DATABASE_URL"]; +if (!url) throw new Error("DATABASE_URL is required"); await runMemoryMigrations(url, { log: (line) => console.log(` ${line}`), diff --git a/src/config.ts b/src/config.ts index f34223d..d85eb20 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,14 +3,14 @@ * * This is the low-level engine config consumed by the DB client and internal * services. The SDK's mount-level config (`MemoryConfig`, see - * mount-config.ts) carries this as its `knowledge` sub-object. There is no + * mount-config.ts) carries this as its `memory` sub-object. There is no * standalone server here — the SDK mounts onto a host Interchange app, so there * is no port, service token, or process entrypoint. */ export type EngineConfig = { databaseUrl: string; dbPoolMax: number; - // Must match the language the knowledge_chunk.text_fts column was built + // Must match the language the memory_chunk.text_fts column was built // with (runMemoryMigrations verifies this against the catalog). // Required and concrete: loadMemoryConfig / createMemory resolve // the default (DEFAULT_FTS_LANGUAGE) once via parseFtsLanguage so services @@ -18,7 +18,7 @@ export type EngineConfig = { // DEFAULT_FTS_LANGUAGE (or parseFtsLanguage(undefined)) explicitly. ftsLanguage: string; // A model endpoint (embed or rerank) is just a URL + capability options, - // trusted the same as KNOWLEDGE_DATABASE_URL — including a self-hosted endpoint on + // trusted the same as DATABASE_URL — including a self-hosted endpoint on // localhost or a private IP. Self-hosted or managed makes no difference: // there is no self-host flag anywhere in the engine. embed: { diff --git a/src/core/adapt-and-plan.ts b/src/core/adapt-and-plan.ts index bc1ea66..8836077 100644 --- a/src/core/adapt-and-plan.ts +++ b/src/core/adapt-and-plan.ts @@ -7,7 +7,7 @@ import type { AdaptedDocumentChunk, EntityHint, } from "./schemas/adapted-document.ts"; -import type { KnowledgeEdgeHint } from "./schemas/entity-edge.ts"; +import type { MemoryEdgeHint } from "./schemas/entity-edge.ts"; /** * Thrown when an AdaptedDocument fails adaptAndPlan's own defensive checks @@ -37,7 +37,7 @@ export type CapturePlan = { contentHash: string; chunks: CapturePlanChunk[]; entityHints: EntityHint[]; - edges: KnowledgeEdgeHint[]; + edges: MemoryEdgeHint[]; }; export type AdaptAndPlanOptions = { diff --git a/src/core/embed-client.ts b/src/core/embed-client.ts index 1df11a1..21692dd 100644 --- a/src/core/embed-client.ts +++ b/src/core/embed-client.ts @@ -4,7 +4,7 @@ import { type } from "arktype"; // Text Embeddings Inference (TEI) server, or a self-hosted Ollama instance. // `apiKey` is an already-resolved secret string: resolving it from the // tenant credential store is the CALLER's job. A `baseUrl` is just a trusted -// URL, the same as `KNOWLEDGE_DATABASE_URL` — self-hosted or managed makes no +// URL, the same as `DATABASE_URL` — self-hosted or managed makes no // difference, and there is no self-host flag anywhere. export const EmbedClientConfigSchema = type({ baseUrl: "string", diff --git a/src/core/embed-model-registry.test.ts b/src/core/embed-model-registry.test.ts index 0b369c5..19cd032 100644 --- a/src/core/embed-model-registry.test.ts +++ b/src/core/embed-model-registry.test.ts @@ -64,7 +64,7 @@ describe("computeModelKey / embeddingTableName", () => { expect(key).toMatch(/^[a-f0-9]{16}$/); const tableName = embeddingTableName(key); expect(tableName).toMatch(EMBED_TABLE_NAME_PATTERN); - expect(tableName).toBe(`"knowledge"."embedding_${key}"`); + expect(tableName).toBe(`"memory"."embedding_${key}"`); }); it("rejects a key that would produce an invalid identifier", () => { @@ -134,7 +134,7 @@ describe("activateEmbedModel", () => { expect(result.tableName).toMatch(EMBED_TABLE_NAME_PATTERN); const insertQuery = queries.find((q) => - q.sql.includes('INSERT INTO "knowledge"."embed_model"'), + q.sql.includes('INSERT INTO "memory"."embed_model"'), ); expect(insertQuery).toBeDefined(); expect(insertQuery?.params).toContain("tenant-1"); @@ -143,10 +143,10 @@ describe("activateEmbedModel", () => { const createTableQuery = queries.find((q) => q.sql.includes("CREATE TABLE IF NOT EXISTS")); expect(createTableQuery?.sql).toContain(result.tableName); expect(createTableQuery?.sql).toContain("vector(768)"); - const bare = result.tableName.replace(/^"knowledge"\."|"$/g, ""); + const bare = result.tableName.replace(/^"memory"\."|"$/g, ""); expect(createTableQuery?.sql).toContain(`CONSTRAINT ${bare}_chunk_fk`); expect(createTableQuery?.sql).toContain( - 'FOREIGN KEY (chunk_id) REFERENCES "knowledge"."chunk" (id) ON DELETE CASCADE', + 'FOREIGN KEY (chunk_id) REFERENCES "memory"."chunk" (id) ON DELETE CASCADE', ); const tenantIndexQuery = queries.find((q) => q.sql.includes("_tenant_chunk_idx")); @@ -269,7 +269,7 @@ describe("resolveActiveEmbedTable", () => { }; const result = await resolveActiveEmbedTable(client, "tenant-1"); expect(result).toEqual({ - tableName: `"knowledge"."embedding_${modelKey}"`, + tableName: `"memory"."embedding_${modelKey}"`, dims: 768, modelId: baseConfig.modelId, }); diff --git a/src/core/embed-model-registry.ts b/src/core/embed-model-registry.ts index e984747..147e125 100644 --- a/src/core/embed-model-registry.ts +++ b/src/core/embed-model-registry.ts @@ -31,7 +31,7 @@ export function cosineDistanceExpr( } // Dims are dynamic and discovered, never hard-coded — the dimension travels -// with exactly one artifact: knowledge.embed_model.dims, discovered here at +// with exactly one artifact: memory.embed_model.dims, discovered here at // configure time. Never resurrect an EMBED_DIM constant anywhere in this module. export const MIN_EMBED_DIMS = 64; // Upper bound is pgvector's halfvec index cap: above 4000 dims no index type @@ -73,10 +73,10 @@ export const EMBED_TABLE_BARE_PATTERN = /^embedding_[a-f0-9]{16}$/; /** * Fully schema-qualified embedding table name for raw SQL interpolation. - * Tables live under the knowledge schema: "knowledge"."embedding_". + * Tables live under the memory schema: "memory"."embedding_". */ export const EMBED_TABLE_NAME_PATTERN = - /^"knowledge"\."embedding_[a-f0-9]{16}"$/; + /^"memory"\."embedding_[a-f0-9]{16}"$/; // This is the only place in this module that ever interpolates a computed // identifier into raw SQL (see activateEmbedModel below) — a future change @@ -93,7 +93,7 @@ export function embeddingTableBareName(modelKey: string): string { export function embeddingTableName(modelKey: string): string { const bare = embeddingTableBareName(modelKey); - return `"knowledge"."${bare}"`; + return `"memory"."${bare}"`; } // Minimal DB seam — this module takes no dependency on drizzle-orm/postgres @@ -123,7 +123,7 @@ export async function activateEmbedModel( const bare = embeddingTableBareName(modelKey); await client.query( - `INSERT INTO "knowledge"."embed_model" (id, tenant_id, model_key, model_id, dims, status, created_at, updated_at) + `INSERT INTO "memory"."embed_model" (id, tenant_id, model_key, model_id, dims, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, 'active', now(), now()) ON CONFLICT (tenant_id, model_key) DO UPDATE SET model_id = EXCLUDED.model_id, dims = EXCLUDED.dims, updated_at = now()`, @@ -142,7 +142,7 @@ export async function activateEmbedModel( tenant_id text NOT NULL, embedding vector(${dims}), CONSTRAINT ${bare}_chunk_fk - FOREIGN KEY (chunk_id) REFERENCES "knowledge"."chunk" (id) ON DELETE CASCADE + FOREIGN KEY (chunk_id) REFERENCES "memory"."chunk" (id) ON DELETE CASCADE )`, [], ); @@ -203,7 +203,7 @@ export async function resolveActiveEmbedTable( tenantId: string, ): Promise { const rows = await client.query( - `SELECT model_key, model_id, dims FROM "knowledge"."embed_model" + `SELECT model_key, model_id, dims FROM "memory"."embed_model" WHERE tenant_id = $1 AND status = 'active' ORDER BY updated_at DESC LIMIT 1`, diff --git a/src/core/embed-worker.ts b/src/core/embed-worker.ts index ebdd85d..135d4d2 100644 --- a/src/core/embed-worker.ts +++ b/src/core/embed-worker.ts @@ -30,7 +30,7 @@ function assertValidTableName(tableName: string): void { /** * Embeds and stores vectors for a known, already-inserted set of - * knowledge_chunk rows — the capture service's counterpart to a pending-chunk + * memory_chunk rows — the capture service's counterpart to a pending-chunk * scanner: the caller already knows exactly which chunks are new (it just * inserted them), so there is no LEFT JOIN discovery step here. * diff --git a/src/core/engine-client-config.ts b/src/core/engine-client-config.ts index cd48c5e..8c31564 100644 --- a/src/core/engine-client-config.ts +++ b/src/core/engine-client-config.ts @@ -17,7 +17,7 @@ const VALID_EMBED_API_STYLES = new Set(["openai", "tei", "ollama"]); // the trust boundary between config and the client — an invalid value is an // operator misconfiguration and must fail loudly, not silently degrade. // Built from the engine's own operator-configured embed endpoint — a trusted -// URL, the same as KNOWLEDGE_DATABASE_URL. +// URL, the same as DATABASE_URL. export function toEmbedClientConfig( embed: EngineConfig["embed"], ): EmbedClientConfig { @@ -41,7 +41,7 @@ export function toEmbedClientConfig( // `"tei"` below. Absent `baseUrl` => rerank is unconfigured => `undefined`, // same degrade-soft precedent as the embed config being absent upstream. // Built from the engine's own operator-configured rerank endpoint — a trusted -// URL, the same as KNOWLEDGE_DATABASE_URL. +// URL, the same as DATABASE_URL. export function toRerankClientConfig( rerank: EngineConfig["rerank"], ): RerankClientConfig | undefined { diff --git a/src/core/fts-language.test.ts b/src/core/fts-language.test.ts index a997f9e..58d73fd 100644 --- a/src/core/fts-language.test.ts +++ b/src/core/fts-language.test.ts @@ -32,7 +32,7 @@ describe("parseFtsLanguage", () => { describe("the baseline migration language token", () => { it("is present in the generated-column DDL, ready for substitution", async () => { const ddl = await readFile( - join(import.meta.dir, "..", "..", "migrations", "0002_knowledge_baseline.sql"), + join(import.meta.dir, "..", "..", "migrations", "0002_memory_baseline.sql"), "utf8", ); expect(ddl).toContain(`to_tsvector('${FTS_LANGUAGE_TOKEN}', "text")`); diff --git a/src/core/fts-language.ts b/src/core/fts-language.ts index d4202fc..0a3ee68 100644 --- a/src/core/fts-language.ts +++ b/src/core/fts-language.ts @@ -72,14 +72,14 @@ export function createFtsVerification( function rebuildColumnRecipe(language: string): string { return ( ` BEGIN;\n` + - ` DROP INDEX IF EXISTS "knowledge"."chunk_text_fts_idx";\n` + - ` ALTER TABLE "knowledge"."chunk" DROP COLUMN text_fts;\n` + - ` ALTER TABLE "knowledge"."chunk" ADD COLUMN text_fts tsvector\n` + + ` DROP INDEX IF EXISTS "memory"."chunk_text_fts_idx";\n` + + ` ALTER TABLE "memory"."chunk" DROP COLUMN text_fts;\n` + + ` ALTER TABLE "memory"."chunk" ADD COLUMN text_fts tsvector\n` + ` GENERATED ALWAYS AS (to_tsvector('${language}', "text")) STORED;\n` + ` COMMIT;\n\n` + ` -- Separate statement/connection — CANNOT run inside the transaction\n` + ` -- above, or any transaction block, ever:\n` + - ` CREATE INDEX CONCURRENTLY "knowledge"."chunk_text_fts_idx" ON "knowledge"."chunk" USING gin (text_fts);\n\n` + + ` CREATE INDEX CONCURRENTLY "memory"."chunk_text_fts_idx" ON "memory"."chunk" USING gin (text_fts);\n\n` + `Both ALTER TABLE statements take an ACCESS EXCLUSIVE lock and rewrite the table ` + `(DROP COLUMN then re-adding a STORED generated column forces a full rewrite) — ` + `expect a stall on this table for the duration on a populated database; run during a maintenance window.` @@ -92,7 +92,7 @@ export interface FtsVerifySqlClient { /** * Enforce the invariant the env var alone cannot: the language baked into - * knowledge.chunk.text_fts (read back from the catalog — the authoritative + * memory.chunk.text_fts (read back from the catalog — the authoritative * record of what the DDL actually applied) must equal the configured one, * and the configured one must be an installed text search config. Throws * with a rebuild instruction on mismatch. Run at startup (the migration @@ -116,13 +116,13 @@ export async function verifyFtsLanguage( `SELECT pg_get_expr(d.adbin, d.adrelid) AS expr FROM pg_attrdef d JOIN pg_attribute a ON a.attrelid = d.adrelid AND a.attnum = d.adnum - WHERE d.adrelid = '"knowledge"."chunk"'::regclass AND a.attname = 'text_fts'`, + WHERE d.adrelid = '"memory"."chunk"'::regclass AND a.attname = 'text_fts'`, [], ); const expr = rows[0]?.["expr"]; if (typeof expr !== "string") { throw new Error( - 'knowledge.chunk.text_fts has no generation expression — schema not migrated?', + 'memory.chunk.text_fts has no generation expression — schema not migrated?', ); } // Only unqualified `pg_catalog` configs are supported: FTS_LANGUAGE_PATTERN @@ -135,13 +135,13 @@ export async function verifyFtsLanguage( const match = APPLIED_REGCONFIG_RE.exec(expr); if (match === null) { throw new Error( - `Could not read the applied FTS language from knowledge.chunk.text_fts: ${expr}`, + `Could not read the applied FTS language from memory.chunk.text_fts: ${expr}`, ); } const [, schema, applied] = match; if (schema !== undefined) { throw new Error( - `knowledge.chunk.text_fts was built with the schema-qualified text search config "${schema}.${applied}", ` + + `memory.chunk.text_fts was built with the schema-qualified text search config "${schema}.${applied}", ` + `but FTS_LANGUAGE only supports unqualified pg_catalog configs. ` + `Either drop the schema qualification (move/alias the config into pg_catalog), or rebuild the column ` + `under an unqualified config name:\n\n${rebuildColumnRecipe(ftsLanguage)}`, @@ -149,7 +149,7 @@ export async function verifyFtsLanguage( } if (applied !== ftsLanguage) { throw new Error( - `FTS language mismatch: knowledge.chunk.text_fts was built with "${applied}" but the configuration says "${ftsLanguage}". ` + + `FTS language mismatch: memory.chunk.text_fts was built with "${applied}" but the configuration says "${ftsLanguage}". ` + `Search would silently stem queries differently than the index.\n\n` + `To rebuild the column under the new language:\n\n${rebuildColumnRecipe(ftsLanguage)}\n\n` + `Or, fix FTS_LANGUAGE back to "${applied}" instead.`, diff --git a/src/core/generation.ts b/src/core/generation.ts index 80884bf..3be8af5 100644 --- a/src/core/generation.ts +++ b/src/core/generation.ts @@ -1,4 +1,4 @@ -// The replay-generation tag every knowledge_version row carries. The normal /capture +// The replay-generation tag every memory_version row carries. The normal /capture // path always writes this generation; a replay (transform-run) writes its // own generation (the run id) instead, so a replayed corpus never touches or // is visible alongside the live one unless a caller explicitly asks for it. diff --git a/src/core/schemas/adapted-document.ts b/src/core/schemas/adapted-document.ts index 00f0071..2dd5d3c 100644 --- a/src/core/schemas/adapted-document.ts +++ b/src/core/schemas/adapted-document.ts @@ -1,10 +1,10 @@ import { type } from "arktype"; import { CreatedByKindSchema } from "./document.ts"; -import { KnowledgeEdgeHintSchema } from "./entity-edge.ts"; +import { MemoryEdgeHintSchema } from "./entity-edge.ts"; import { AuthoritySourceClassSchema } from "../authority.ts"; // A hint that a chunk/document mentions a real-world entity; the ingestion -// engine resolves this against knowledge_entity, creating a row if none +// engine resolves this against memory_entity, creating a row if none // matches yet. export const EntityHintSchema = type({ kind: "string", @@ -56,7 +56,7 @@ export const AdaptedDocumentSchema = type({ accessTags: "string[]", "attributes?": "Record", entityHints: EntityHintSchema.array(), - "edges?": KnowledgeEdgeHintSchema.array(), + "edges?": MemoryEdgeHintSchema.array(), chunks: AdaptedDocumentChunkSchema.array().atMostLength(MAX_CHUNKS_PER_DOCUMENT), "rawPointer?": RawPointerSchema, "actor?": ActorAttributionSchema, diff --git a/src/core/schemas/chunk.test.ts b/src/core/schemas/chunk.test.ts index d1ccdcb..4ea3eb4 100644 --- a/src/core/schemas/chunk.test.ts +++ b/src/core/schemas/chunk.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "bun:test"; import { type } from "arktype"; -import { KnowledgeChunkSchema } from "./chunk.ts"; +import { MemoryChunkSchema } from "./chunk.ts"; -describe("KnowledgeChunkSchema", () => { +describe("MemoryChunkSchema", () => { it("round-trips a full fixture", () => { const fixture = { id: "chunk_1", @@ -14,12 +14,12 @@ describe("KnowledgeChunkSchema", () => { role: "summary", created_at: "2026-07-19T00:00:00.000Z", }; - const out = KnowledgeChunkSchema(fixture); + const out = MemoryChunkSchema(fixture); expect(out instanceof type.errors ? out.summary : out).toEqual(fixture); }); it("parses without the optional role", () => { - const out = KnowledgeChunkSchema({ + const out = MemoryChunkSchema({ id: "chunk_1", tenant_id: "tenant_1", version_id: "kv_1", @@ -32,7 +32,7 @@ describe("KnowledgeChunkSchema", () => { }); it("rejects a chunk missing version_id", () => { - const out = KnowledgeChunkSchema({ + const out = MemoryChunkSchema({ id: "chunk_1", tenant_id: "tenant_1", document_id: "doc_1", diff --git a/src/core/schemas/chunk.ts b/src/core/schemas/chunk.ts index 8cf6b9e..ebdd3b6 100644 --- a/src/core/schemas/chunk.ts +++ b/src/core/schemas/chunk.ts @@ -2,7 +2,7 @@ import { type } from "arktype"; // chunk_id is a hash of (document_id, version, ordinal); chunks are never // reused across versions. -export const KnowledgeChunkSchema = type({ +export const MemoryChunkSchema = type({ id: "string", tenant_id: "string", version_id: "string", @@ -12,4 +12,4 @@ export const KnowledgeChunkSchema = type({ "role?": "string", created_at: "string", }); -export type KnowledgeChunk = typeof KnowledgeChunkSchema.infer; +export type MemoryChunk = typeof MemoryChunkSchema.infer; diff --git a/src/core/schemas/document.test.ts b/src/core/schemas/document.test.ts index dde5a25..4645578 100644 --- a/src/core/schemas/document.test.ts +++ b/src/core/schemas/document.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from "bun:test"; import { type } from "arktype"; import { - KnowledgeDocumentSchema, - KnowledgeVersionSchema, + MemoryDocumentSchema, + MemoryVersionSchema, } from "./document.ts"; -import type { KnowledgeDocument, KnowledgeVersion } from "./document.ts"; +import type { MemoryDocument, MemoryVersion } from "./document.ts"; -describe("KnowledgeDocumentSchema", () => { +describe("MemoryDocumentSchema", () => { it("round-trips a full fixture", () => { - const fixture: KnowledgeDocument = { + const fixture: MemoryDocument = { id: "doc_1", tenant_id: "tenant_1", kind: "call_transcript", @@ -20,12 +20,12 @@ describe("KnowledgeDocumentSchema", () => { created_at: "2026-07-19T00:00:00.000Z", last_seen_at: "2026-07-19T00:00:00.000Z", }; - const out = KnowledgeDocumentSchema(fixture); + const out = MemoryDocumentSchema(fixture); expect(out instanceof type.errors ? out.summary : out).toEqual(fixture); }); it("rejects a document missing external_ref", () => { - const out = KnowledgeDocumentSchema({ + const out = MemoryDocumentSchema({ id: "doc_1", tenant_id: "tenant_1", kind: "call_transcript", @@ -40,9 +40,9 @@ describe("KnowledgeDocumentSchema", () => { }); }); -describe("KnowledgeVersionSchema", () => { +describe("MemoryVersionSchema", () => { it("round-trips a full fixture", () => { - const fixture: KnowledgeVersion = { + const fixture: MemoryVersion = { id: "kv_1", tenant_id: "tenant_1", document_id: "doc_1", @@ -58,12 +58,12 @@ describe("KnowledgeVersionSchema", () => { created_by_principal_id: "principal_1", created_by_kind: "human", }; - const out = KnowledgeVersionSchema(fixture); + const out = MemoryVersionSchema(fixture); expect(out instanceof type.errors ? out.summary : out).toEqual(fixture); }); it("rejects an invalid status", () => { - const out = KnowledgeVersionSchema({ + const out = MemoryVersionSchema({ id: "kv_1", tenant_id: "tenant_1", document_id: "doc_1", diff --git a/src/core/schemas/document.ts b/src/core/schemas/document.ts index 635421d..4a7725b 100644 --- a/src/core/schemas/document.ts +++ b/src/core/schemas/document.ts @@ -1,16 +1,16 @@ import { type } from "arktype"; -export const KnowledgeVersionStatusSchema = type( +export const MemoryVersionStatusSchema = type( "'active'|'superseded'|'deprecated'|'archived'|'tombstoned'", ); -export type KnowledgeVersionStatus = typeof KnowledgeVersionStatusSchema.infer; +export type MemoryVersionStatus = typeof MemoryVersionStatusSchema.infer; export const CreatedByKindSchema = type("'human'|'agent'|'system'|'adapter'"); export type CreatedByKind = typeof CreatedByKindSchema.infer; // The stable logical row for a captured source, deduped on (tenant_id, // adapter, external_ref). Document access is grant tags only. -export const KnowledgeDocumentSchema = type({ +export const MemoryDocumentSchema = type({ id: "string", tenant_id: "string", kind: "string", @@ -22,18 +22,18 @@ export const KnowledgeDocumentSchema = type({ created_at: "string", last_seen_at: "string", }); -export type KnowledgeDocument = typeof KnowledgeDocumentSchema.infer; +export type MemoryDocument = typeof MemoryDocumentSchema.infer; // The versioned body of a document. Chunks belong to a version_id, never // reused across versions. -export const KnowledgeVersionSchema = type({ +export const MemoryVersionSchema = type({ id: "string", tenant_id: "string", document_id: "string", version: "number", version_id: "string", supersedes_version_id: "string | null", - status: KnowledgeVersionStatusSchema, + status: MemoryVersionStatusSchema, content_hash: "string", occurred_at: "string", ingested_at: "string", @@ -43,4 +43,4 @@ export const KnowledgeVersionSchema = type({ created_by_kind: CreatedByKindSchema, "generator_agent_id?": "string", }); -export type KnowledgeVersion = typeof KnowledgeVersionSchema.infer; +export type MemoryVersion = typeof MemoryVersionSchema.infer; diff --git a/src/core/schemas/entity-edge.test.ts b/src/core/schemas/entity-edge.test.ts index 2477770..7635b4b 100644 --- a/src/core/schemas/entity-edge.test.ts +++ b/src/core/schemas/entity-edge.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it } from "bun:test"; import { type } from "arktype"; import { - KnowledgeEdgeHintSchema, - KnowledgeEdgeSchema, - KnowledgeEntitySchema, + MemoryEdgeHintSchema, + MemoryEdgeSchema, + MemoryEntitySchema, } from "./entity-edge.ts"; -import type { KnowledgeEdge } from "./entity-edge.ts"; +import type { MemoryEdge } from "./entity-edge.ts"; -describe("KnowledgeEntitySchema", () => { +describe("MemoryEntitySchema", () => { it("round-trips a full fixture", () => { const fixture = { id: "entity_1", @@ -16,14 +16,14 @@ describe("KnowledgeEntitySchema", () => { identifiers: { email: "jane@example.com" }, created_at: "2026-07-19T00:00:00.000Z", }; - const out = KnowledgeEntitySchema(fixture); + const out = MemoryEntitySchema(fixture); expect(out instanceof type.errors ? out.summary : out).toEqual(fixture); }); }); -describe("KnowledgeEdgeSchema", () => { +describe("MemoryEdgeSchema", () => { it("round-trips a full fixture", () => { - const fixture: KnowledgeEdge = { + const fixture: MemoryEdge = { id: "edge_1", tenant_id: "tenant_1", rel: "about", @@ -31,12 +31,12 @@ describe("KnowledgeEdgeSchema", () => { to: { type: "entity", ref: "entity_1" }, created_at: "2026-07-19T00:00:00.000Z", }; - const out = KnowledgeEdgeSchema(fixture); + const out = MemoryEdgeSchema(fixture); expect(out instanceof type.errors ? out.summary : out).toEqual(fixture); }); it("rejects an unknown rel", () => { - const out = KnowledgeEdgeSchema({ + const out = MemoryEdgeSchema({ id: "edge_1", tenant_id: "tenant_1", rel: "orbits", @@ -48,10 +48,10 @@ describe("KnowledgeEdgeSchema", () => { }); }); -// T4 — a KnowledgeEdgeHint missing to.ref must fail arktype validation. -describe("KnowledgeEdgeHintSchema", () => { +// T4 — a MemoryEdgeHint missing to.ref must fail arktype validation. +describe("MemoryEdgeHintSchema", () => { it("parses a full fixture", () => { - const out = KnowledgeEdgeHintSchema({ + const out = MemoryEdgeHintSchema({ rel: "produced_by", to: { type: "native", ref: "principal_1" }, }); @@ -59,7 +59,7 @@ describe("KnowledgeEdgeHintSchema", () => { }); it("rejects a hint whose to is missing ref", () => { - const out = KnowledgeEdgeHintSchema({ + const out = MemoryEdgeHintSchema({ rel: "produced_by", to: { type: "native" }, }); diff --git a/src/core/schemas/entity-edge.ts b/src/core/schemas/entity-edge.ts index 0052138..ff12c21 100644 --- a/src/core/schemas/entity-edge.ts +++ b/src/core/schemas/entity-edge.ts @@ -3,45 +3,45 @@ import { type } from "arktype"; // A real-world thing (person, org, deal, ...) a document or chunk mentions. // Kept lightweight — identity keys only (email, domain, ...), not another // copy of chunk text. -export const KnowledgeEntitySchema = type({ +export const MemoryEntitySchema = type({ id: "string", tenant_id: "string", kind: "string", identifiers: "Record", created_at: "string", }); -export type KnowledgeEntity = typeof KnowledgeEntitySchema.infer; +export type MemoryEntity = typeof MemoryEntitySchema.infer; -export const KnowledgeEdgeRefTypeSchema = type("'document'|'entity'|'native'"); -export type KnowledgeEdgeRefType = typeof KnowledgeEdgeRefTypeSchema.infer; +export const MemoryEdgeRefTypeSchema = type("'document'|'entity'|'native'"); +export type MemoryEdgeRefType = typeof MemoryEdgeRefTypeSchema.infer; -export const KnowledgeEdgeRelSchema = type( +export const MemoryEdgeRelSchema = type( "'about'|'produced_by'|'links'|'parent'|'mentions'|'waiting_on'", ); -export type KnowledgeEdgeRel = typeof KnowledgeEdgeRelSchema.infer; +export type MemoryEdgeRel = typeof MemoryEdgeRelSchema.infer; -export const KnowledgeEdgeRefSchema = type({ - type: KnowledgeEdgeRefTypeSchema, +export const MemoryEdgeRefSchema = type({ + type: MemoryEdgeRefTypeSchema, ref: "string", }); -export type KnowledgeEdgeRef = typeof KnowledgeEdgeRefSchema.infer; +export type MemoryEdgeRef = typeof MemoryEdgeRefSchema.infer; // Graph structure between documents/entities/native refs (e.g. principals). // Lightweight rows only. -export const KnowledgeEdgeSchema = type({ +export const MemoryEdgeSchema = type({ id: "string", tenant_id: "string", - rel: KnowledgeEdgeRelSchema, - from: KnowledgeEdgeRefSchema, - to: KnowledgeEdgeRefSchema, + rel: MemoryEdgeRelSchema, + from: MemoryEdgeRefSchema, + to: MemoryEdgeRefSchema, created_at: "string", }); -export type KnowledgeEdge = typeof KnowledgeEdgeSchema.infer; +export type MemoryEdge = typeof MemoryEdgeSchema.infer; // The edge hint an adapter emits on an AdaptedDocument — "from" is implicit // (the document being adapted), so only "rel" and "to" are carried. -export const KnowledgeEdgeHintSchema = type({ - rel: KnowledgeEdgeRelSchema, - to: KnowledgeEdgeRefSchema, +export const MemoryEdgeHintSchema = type({ + rel: MemoryEdgeRelSchema, + to: MemoryEdgeRefSchema, }); -export type KnowledgeEdgeHint = typeof KnowledgeEdgeHintSchema.infer; +export type MemoryEdgeHint = typeof MemoryEdgeHintSchema.infer; diff --git a/src/core/schemas/search.ts b/src/core/schemas/search.ts index 0f3a2c9..22c5c7e 100644 --- a/src/core/schemas/search.ts +++ b/src/core/schemas/search.ts @@ -1,5 +1,5 @@ import { type } from "arktype"; -import { CreatedByKindSchema, KnowledgeVersionStatusSchema } from "./document.ts"; +import { CreatedByKindSchema, MemoryVersionStatusSchema } from "./document.ts"; // The retrieval contract locked on day one. A SearchHit always pins a // version_id (a citation must be reproducible against the exact version it @@ -31,7 +31,7 @@ export const SearchHitSchema = type({ document_id: "string", version: "number", version_id: "string", - status: KnowledgeVersionStatusSchema, + status: MemoryVersionStatusSchema, score: "number", title: "string", snippet: "string", diff --git a/src/db/schema.test.ts b/src/db/schema.test.ts index 3508c5d..36489f7 100644 --- a/src/db/schema.test.ts +++ b/src/db/schema.test.ts @@ -1,59 +1,59 @@ /** - * CL-5233: all engine tables live under the knowledge Postgres schema. + * CL-5233: all engine tables live under the memory Postgres schema. Renamed from `knowledge` to `memory` for CL-6009. */ import { describe, expect, it } from "bun:test"; import { getTableName } from "drizzle-orm"; import { - KNOWLEDGE_SCHEMA, - knowledgeChunk, - knowledgeDocument, - knowledgeEdge, - knowledgeEmbedModel, - knowledgeEntity, - knowledgeSchema, - knowledgeVersion, + MEMORY_SCHEMA, + memoryChunk, + memoryDocument, + memoryEdge, + memoryEmbedModel, + memoryEntity, + memorySchema, + memoryVersion, rawCapture, transformConfig, transformRun, } from "./schema.ts"; const TABLES = [ - knowledgeDocument, - knowledgeVersion, - knowledgeChunk, - knowledgeEntity, - knowledgeEdge, + memoryDocument, + memoryVersion, + memoryChunk, + memoryEntity, + memoryEdge, rawCapture, - knowledgeEmbedModel, + memoryEmbedModel, transformConfig, transformRun, ] as const; -describe("knowledge Postgres schema qualification (CL-5233)", () => { - it("exports KNOWLEDGE_SCHEMA = knowledge", () => { - expect(KNOWLEDGE_SCHEMA).toBe("knowledge"); - expect(knowledgeSchema.schemaName).toBe("knowledge"); +describe("memory Postgres schema qualification (CL-5233)", () => { + it("exports MEMORY_SCHEMA = memory", () => { + expect(MEMORY_SCHEMA).toBe("memory"); + expect(memorySchema.schemaName).toBe("memory"); }); - it("every table is registered under the knowledge schema", () => { + it("every table is registered under the memory schema", () => { for (const table of TABLES) { // drizzle Table internal schema key const schemaName = (table as unknown as { [key: symbol]: unknown })[ Symbol.for("drizzle:Schema") ]; - expect(schemaName).toBe("knowledge"); - // bare names drop the redundant knowledge_ prefix - expect(getTableName(table)).not.toMatch(/^knowledge_/); + expect(schemaName).toBe("memory"); + // bare names drop the redundant memory_ prefix + expect(getTableName(table)).not.toMatch(/^memory_/); } }); - it("maps legacy knowledge_* names to short table names", () => { - expect(getTableName(knowledgeDocument)).toBe("document"); - expect(getTableName(knowledgeVersion)).toBe("version"); - expect(getTableName(knowledgeChunk)).toBe("chunk"); - expect(getTableName(knowledgeEntity)).toBe("entity"); - expect(getTableName(knowledgeEdge)).toBe("edge"); - expect(getTableName(knowledgeEmbedModel)).toBe("embed_model"); + it("maps legacy memory_* names to short table names", () => { + expect(getTableName(memoryDocument)).toBe("document"); + expect(getTableName(memoryVersion)).toBe("version"); + expect(getTableName(memoryChunk)).toBe("chunk"); + expect(getTableName(memoryEntity)).toBe("entity"); + expect(getTableName(memoryEdge)).toBe("edge"); + expect(getTableName(memoryEmbedModel)).toBe("embed_model"); expect(getTableName(rawCapture)).toBe("raw_capture"); expect(getTableName(transformConfig)).toBe("transform_config"); expect(getTableName(transformRun)).toBe("transform_run"); diff --git a/src/db/schema.ts b/src/db/schema.ts index d4dde48..d69594e 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -12,9 +12,9 @@ import { } from "drizzle-orm/pg-core"; /** Postgres schema owned by this package — never public. */ -export const KNOWLEDGE_SCHEMA = "knowledge"; +export const MEMORY_SCHEMA = "memory"; -export const knowledgeSchema = pgSchema(KNOWLEDGE_SCHEMA); +export const memorySchema = pgSchema(MEMORY_SCHEMA); // No built-in `bytea` helper in drizzle-orm/pg-core; raw_capture.raw_bytes // holds non-textual raw payloads (binary source formats) as a Buffer. @@ -24,7 +24,7 @@ const bytea = customType<{ data: Buffer }>({ }, }); -export const knowledgeDocument = knowledgeSchema.table( +export const memoryDocument = memorySchema.table( "document", { id: text("id").primaryKey(), @@ -49,14 +49,14 @@ export const knowledgeDocument = knowledgeSchema.table( ], ); -export const knowledgeVersion = knowledgeSchema.table( +export const memoryVersion = memorySchema.table( "version", { id: text("id").primaryKey(), tenantId: text("tenant_id").notNull(), documentId: text("document_id") .notNull() - .references(() => knowledgeDocument.id, { onDelete: "cascade" }), + .references(() => memoryDocument.id, { onDelete: "cascade" }), version: integer("version").notNull(), supersedesVersionId: text("supersedes_version_id"), status: text("status").notNull().default("active"), @@ -88,17 +88,17 @@ export const knowledgeVersion = knowledgeSchema.table( ], ); -export const knowledgeChunk = knowledgeSchema.table( +export const memoryChunk = memorySchema.table( "chunk", { id: text("id").primaryKey(), tenantId: text("tenant_id").notNull(), versionId: text("version_id") .notNull() - .references(() => knowledgeVersion.id, { onDelete: "cascade" }), + .references(() => memoryVersion.id, { onDelete: "cascade" }), documentId: text("document_id") .notNull() - .references(() => knowledgeDocument.id, { onDelete: "cascade" }), + .references(() => memoryDocument.id, { onDelete: "cascade" }), ordinal: integer("ordinal").notNull(), text: text("text").notNull(), role: text("role"), @@ -109,7 +109,7 @@ export const knowledgeChunk = knowledgeSchema.table( ], ); -export const knowledgeEntity = knowledgeSchema.table( +export const memoryEntity = memorySchema.table( "entity", { id: text("id").primaryKey(), @@ -122,7 +122,7 @@ export const knowledgeEntity = knowledgeSchema.table( (t) => [index("entity_tenant_kind_idx").on(t.tenantId, t.kind)], ); -export const knowledgeEdge = knowledgeSchema.table( +export const memoryEdge = memorySchema.table( "edge", { id: text("id").primaryKey(), @@ -145,7 +145,7 @@ export const knowledgeEdge = knowledgeSchema.table( // different config without re-fetching source. Append-only; dedupe on // (tenantId, sourceHash) reuses the existing row instead of inserting a // duplicate. -export const rawCapture = knowledgeSchema.table( +export const rawCapture = memorySchema.table( "raw_capture", { id: text("id").primaryKey(), @@ -172,7 +172,7 @@ export const rawCapture = knowledgeSchema.table( ], ); -export const knowledgeEmbedModel = knowledgeSchema.table("embed_model", { +export const memoryEmbedModel = memorySchema.table("embed_model", { id: text("id").primaryKey(), tenantId: text("tenant_id").notNull(), modelKey: text("model_key").notNull(), @@ -188,7 +188,7 @@ export const knowledgeEmbedModel = knowledgeSchema.table("embed_model", { // + retrieval-boost config (see src/core/schemas/transform.ts); unique on // (tenant_id, name, version) so re-creating the same name mints a new // version rather than colliding. -export const transformConfig = knowledgeSchema.table( +export const transformConfig = memorySchema.table( "transform_config", { id: text("id").primaryKey(), @@ -209,9 +209,9 @@ export const transformConfig = knowledgeSchema.table( // The replay pipeline — one execution of a transform_config against a (possibly filtered) // slice of raw_capture. `generation` is this run's id, written onto every -// knowledge_version row it derives; unique so a generation always resolves +// memory_version row it derives; unique so a generation always resolves // back to exactly one run (and therefore one config) at search time. -export const transformRun = knowledgeSchema.table( +export const transformRun = memorySchema.table( "transform_run", { id: text("id").primaryKey(), diff --git a/src/log.ts b/src/log.ts index cc1ac12..cd2d9d1 100644 --- a/src/log.ts +++ b/src/log.ts @@ -1,6 +1,6 @@ import { getLogger } from "@intx/log"; -/** Category-bound logger for the knowledge engine (uses the host's sinks). */ +/** Category-bound logger for the memory engine (uses the host's sinks). */ export const log = getLogger(["memory"]); // Some `@intx/log` sinks do not render a call's context object into the diff --git a/src/memory.ts b/src/memory.ts index 0d00076..e1d2f9f 100644 --- a/src/memory.ts +++ b/src/memory.ts @@ -609,7 +609,7 @@ function createPlaneFromStore( if (!options.textExtractor) { throw new MemoryError( 400, - "file requires a textExtractor on the knowledge plane", + "file requires a textExtractor on the memory plane", ); } const extracted = await options.textExtractor.extract({ @@ -755,8 +755,8 @@ function createEngineDocumentStore(config: MemoryConfig): DocumentStore { SELECT d.id, d.access_tags, v.created_by_principal_id AS created_by - FROM "knowledge"."document" d - LEFT JOIN "knowledge"."version" v + FROM "memory"."document" d + LEFT JOIN "memory"."version" v ON v.document_id = d.id AND v.status = 'active' AND v.generation = 'live' diff --git a/src/migrations.ts b/src/migrations.ts index bb51496..ce3af03 100644 --- a/src/migrations.ts +++ b/src/migrations.ts @@ -1,7 +1,7 @@ /** * Memory-plane (pgvector) schema migrations, callable by host apps. * Applies every migrations/*.sql in filename order, each in its own - * transaction, tracked in knowledge._migrations so re-runs are idempotent + * transaction, tracked in memory._migrations so re-runs are idempotent * and the ledger never collides with a host's public migration bookkeeping. */ import postgres from "postgres"; @@ -13,7 +13,7 @@ import { verifyFtsLanguage, } from "./core/fts-language.ts"; import { createRawSqlClient } from "./core/embed-sql.ts"; -import { KNOWLEDGE_SCHEMA } from "./db/schema.ts"; +import { MEMORY_SCHEMA } from "./db/schema.ts"; const MIGRATIONS_DIR = join(import.meta.dir, "..", "migrations"); @@ -33,16 +33,16 @@ export async function runMemoryMigrations( // Schema first so the ledger and every later migration can land inside it // even when 0001 has not been applied yet (fresh DB) or was skipped. await sql.unsafe( - `CREATE SCHEMA IF NOT EXISTS "${KNOWLEDGE_SCHEMA}"`, + `CREATE SCHEMA IF NOT EXISTS "${MEMORY_SCHEMA}"`, ); await sql.unsafe( - `CREATE TABLE IF NOT EXISTS "${KNOWLEDGE_SCHEMA}"."_migrations" ( + `CREATE TABLE IF NOT EXISTS "${MEMORY_SCHEMA}"."_migrations" ( "name" text PRIMARY KEY, "applied_at" timestamp NOT NULL DEFAULT now() )`, ); const appliedRows = (await sql.unsafe( - `SELECT name FROM "${KNOWLEDGE_SCHEMA}"."_migrations"`, + `SELECT name FROM "${MEMORY_SCHEMA}"."_migrations"`, )) as unknown as { name: string }[]; const applied = new Set(appliedRows.map((row) => row.name)); @@ -60,7 +60,7 @@ export async function runMemoryMigrations( await sql.begin(async (tx) => { await tx.unsafe(ddl); await tx.unsafe( - `INSERT INTO "${KNOWLEDGE_SCHEMA}"."_migrations" (name) VALUES ($1)`, + `INSERT INTO "${MEMORY_SCHEMA}"."_migrations" (name) VALUES ($1)`, [file], ); }); diff --git a/src/mount-config.test.ts b/src/mount-config.test.ts index 84bfa33..5335262 100644 --- a/src/mount-config.test.ts +++ b/src/mount-config.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { loadMemoryConfig } from "./mount-config.ts"; const REQUIRED_ENV = { - KNOWLEDGE_DATABASE_URL: "postgres://localhost:5432/test", + DATABASE_URL: "postgres://localhost:5432/test", EMBED_BASE_URL: "http://embed.example", EMBED_MODEL: "test-model", }; diff --git a/src/mount-config.ts b/src/mount-config.ts index fc593cd..350929d 100644 --- a/src/mount-config.ts +++ b/src/mount-config.ts @@ -54,9 +54,7 @@ function optionalIntEnv(name: string): number | undefined { export function loadMemoryConfig(): MemoryConfig { return { memory: { - // Deliberately no DATABASE_URL fallback: the host app's own database - // must never be mistaken for the engine's vector plane. - databaseUrl: requireEnv("KNOWLEDGE_DATABASE_URL"), + databaseUrl: requireEnv("DATABASE_URL"), dbPoolMax: intEnv("DB_POOL_MAX", 8), ftsLanguage: parseFtsLanguage(optionalEnv("FTS_LANGUAGE")), embed: { diff --git a/src/services/capture.ts b/src/services/capture.ts index 2cfd24b..19a2fb9 100644 --- a/src/services/capture.ts +++ b/src/services/capture.ts @@ -7,11 +7,11 @@ import { formatCaughtError, log } from "../log.ts"; import { stableStringify } from "../core/hash.ts"; import { LIVE_GENERATION } from "../core/generation.ts"; import { - knowledgeChunk, - knowledgeDocument, - knowledgeEdge, - knowledgeEntity, - knowledgeVersion, + memoryChunk, + memoryDocument, + memoryEdge, + memoryEntity, + memoryVersion, rawCapture, } from "../db/schema.ts"; import { @@ -24,7 +24,7 @@ import type { AdaptedDocument, EntityHint, } from "../core/schemas/adapted-document.ts"; -import type { KnowledgeEdgeHint } from "../core/schemas/entity-edge.ts"; +import type { MemoryEdgeHint } from "../core/schemas/entity-edge.ts"; import { createRawSqlClient } from "../core/embed-sql.ts"; import { activateEmbedModel } from "../core/embed-model-registry.ts"; import type { EmbedClientConfig } from "../core/embed-client.ts"; @@ -33,7 +33,7 @@ import { toEmbedClientConfig } from "../core/engine-client-config.ts"; type Tx = Parameters[0]>[0]; -// The single doorway a caller uses to reach the knowledge store: parses an +// The single doorway a caller uses to reach the memory store: parses an // already-adapted document into a capture plan (adaptAndPlan), writes // document/version/chunk/edge rows in one transaction, then embeds the // version's chunks after commit. Unlike a fire-and-forget capture hook, this @@ -95,7 +95,7 @@ async function insertVersion( ): Promise { const versionId = newId("kver"); const authoritySignals = deriveAuthoritySignals(plan); - await tx.insert(knowledgeVersion).values({ + await tx.insert(memoryVersion).values({ id: versionId, tenantId: input.tenantId, documentId, @@ -193,7 +193,7 @@ async function insertOrReuseRawCapture( return existing.id; } -// No unique constraint backs knowledge_entity — dedupe here on an exact +// No unique constraint backs memory_entity — dedupe here on an exact // (tenantId, kind, identifiers) match, matching what a caller re-emits for // the same real-world thing across captures. async function upsertEntity( @@ -205,21 +205,21 @@ async function upsertEntity( const identifiers = { value: hint.identifier }; const rows = await tx .select({ - id: knowledgeEntity.id, - identifiers: knowledgeEntity.identifiers, + id: memoryEntity.id, + identifiers: memoryEntity.identifiers, }) - .from(knowledgeEntity) + .from(memoryEntity) .where( and( - eq(knowledgeEntity.tenantId, tenantId), - eq(knowledgeEntity.kind, hint.kind), + eq(memoryEntity.tenantId, tenantId), + eq(memoryEntity.kind, hint.kind), ), ); const exists = rows.some( (r) => JSON.stringify(r.identifiers) === JSON.stringify(identifiers), ); if (exists) return; - await tx.insert(knowledgeEntity).values({ + await tx.insert(memoryEntity).values({ id: newId("kent"), tenantId, kind: hint.kind, @@ -229,32 +229,32 @@ async function upsertEntity( }); } -// No unique constraint backs knowledge_edge either — dedupe on the full +// No unique constraint backs memory_edge either — dedupe on the full // (tenantId, rel, from, to) tuple so re-ingesting the same document doesn't // pile up duplicate relationship rows across versions. async function upsertEdge( tx: Tx, tenantId: string, documentId: string, - hint: KnowledgeEdgeHint, + hint: MemoryEdgeHint, now: Date, ): Promise { const rows = await tx - .select({ id: knowledgeEdge.id }) - .from(knowledgeEdge) + .select({ id: memoryEdge.id }) + .from(memoryEdge) .where( and( - eq(knowledgeEdge.tenantId, tenantId), - eq(knowledgeEdge.rel, hint.rel), - eq(knowledgeEdge.fromType, "document"), - eq(knowledgeEdge.fromRef, documentId), - eq(knowledgeEdge.toType, hint.to.type), - eq(knowledgeEdge.toRef, hint.to.ref), + eq(memoryEdge.tenantId, tenantId), + eq(memoryEdge.rel, hint.rel), + eq(memoryEdge.fromType, "document"), + eq(memoryEdge.fromRef, documentId), + eq(memoryEdge.toType, hint.to.type), + eq(memoryEdge.toRef, hint.to.ref), ), ) .limit(1); if (rows[0]) return; - await tx.insert(knowledgeEdge).values({ + await tx.insert(memoryEdge).values({ id: newId("kedg"), tenantId, rel: hint.rel, @@ -286,7 +286,7 @@ async function insertChunksAndGraph( })); if (plan.chunks.length > 0) { - await tx.insert(knowledgeChunk).values( + await tx.insert(memoryChunk).values( plan.chunks.map((chunk, i) => ({ id: chunkIds[i] as string, tenantId, @@ -332,12 +332,12 @@ async function deriveVersionInTransaction( const doc = plan.document; const existingRows = await tx .select() - .from(knowledgeDocument) + .from(memoryDocument) .where( and( - eq(knowledgeDocument.tenantId, input.tenantId), - eq(knowledgeDocument.adapter, input.adapter), - eq(knowledgeDocument.externalRef, doc.externalRef), + eq(memoryDocument.tenantId, input.tenantId), + eq(memoryDocument.adapter, input.adapter), + eq(memoryDocument.externalRef, doc.externalRef), ), ) .limit(1); @@ -346,7 +346,7 @@ async function deriveVersionInTransaction( if (!existingDoc) { const documentId = newId("kdoc"); // accessTags is the security boundary for document access. - await tx.insert(knowledgeDocument).values({ + await tx.insert(memoryDocument).values({ id: documentId, tenantId: input.tenantId, kind: doc.kind, @@ -379,23 +379,23 @@ async function deriveVersionInTransaction( const activeVersionRows = await tx .select() - .from(knowledgeVersion) + .from(memoryVersion) .where( and( - eq(knowledgeVersion.documentId, existingDoc.id), - eq(knowledgeVersion.generation, generation), - eq(knowledgeVersion.status, "active"), + eq(memoryVersion.documentId, existingDoc.id), + eq(memoryVersion.generation, generation), + eq(memoryVersion.status, "active"), ), ) - .orderBy(desc(knowledgeVersion.version)) + .orderBy(desc(memoryVersion.version)) .limit(1); const activeVersion = activeVersionRows[0] ?? null; if (activeVersion && activeVersion.contentHash === plan.contentHash) { await tx - .update(knowledgeDocument) + .update(memoryDocument) .set({ lastSeenAt: now }) - .where(eq(knowledgeDocument.id, existingDoc.id)); + .where(eq(memoryDocument.id, existingDoc.id)); return { status: "noop", documentId: existingDoc.id, @@ -405,9 +405,9 @@ async function deriveVersionInTransaction( if (activeVersion) { await tx - .update(knowledgeVersion) + .update(memoryVersion) .set({ status: "superseded" }) - .where(eq(knowledgeVersion.id, activeVersion.id)); + .where(eq(memoryVersion.id, activeVersion.id)); } const versionId = await insertVersion(tx, input, existingDoc.id, plan, { @@ -419,14 +419,14 @@ async function deriveVersionInTransaction( }); await tx - .update(knowledgeDocument) + .update(memoryDocument) .set({ title: doc.title, accessTags: doc.accessTags, attributes: doc.attributes ?? {}, lastSeenAt: now, }) - .where(eq(knowledgeDocument.id, existingDoc.id)); + .where(eq(memoryDocument.id, existingDoc.id)); const insertedChunks = await insertChunksAndGraph( tx, diff --git a/src/services/search.test.ts b/src/services/search.test.ts index ebeb318..e43cae9 100644 --- a/src/services/search.test.ts +++ b/src/services/search.test.ts @@ -231,8 +231,8 @@ describe("fetchDenseCandidates hnsw tuning", () => { unsafe: (sqlText: string) => { statements.push(sqlText); return Promise.resolve( - sqlText.includes('FROM "knowledge"."embed_model"') || - sqlText.includes("FROM knowledge_embed_model") + sqlText.includes('FROM "memory"."embed_model"') || + sqlText.includes("FROM memory_embed_model") ? [MODEL_ROW] : [], ); @@ -406,8 +406,8 @@ describe("fetchDenseCandidates kind/entity filtering", () => { Promise.resolve( // CL-5233 qualified the table; keep the pre-qualify form so an // accidental revert still fails this suite the same way. - sqlText.includes('FROM "knowledge"."embed_model"') || - sqlText.includes("FROM knowledge_embed_model") + sqlText.includes('FROM "memory"."embed_model"') || + sqlText.includes("FROM memory_embed_model") ? [MODEL_ROW] : [], ), diff --git a/src/services/search.ts b/src/services/search.ts index 419119c..636df0a 100644 --- a/src/services/search.ts +++ b/src/services/search.ts @@ -3,10 +3,10 @@ import type { Db, RawSql } from "../db/client.ts"; import type { EngineConfig } from "../config.ts"; import { LIVE_GENERATION } from "../core/generation.ts"; import { - knowledgeChunk, - knowledgeDocument, - knowledgeEdge, - knowledgeVersion, + memoryChunk, + memoryDocument, + memoryEdge, + memoryVersion, } from "../db/schema.ts"; import { createRawSqlClient } from "../core/embed-sql.ts"; import { @@ -146,7 +146,7 @@ const ADAPTER_OPEN_TYPES: Record = { }; // The routable open target: source row via the external_ref suffix when the -// adapter maps to a deep-linkable kind; generic knowledge doc otherwise. +// adapter maps to a deep-linkable kind; generic memory doc otherwise. function openTarget( adapter: string, externalRef: string, @@ -157,7 +157,7 @@ function openTarget( if (openType && externalRef.startsWith(prefix)) { return { type: openType, id: externalRef.slice(prefix.length) }; } - return { type: "knowledge", id: documentId }; + return { type: "memory", id: documentId }; } export function toHit( @@ -285,16 +285,16 @@ export async function attachEntityIds( if (documentIds.length === 0) return new Map(); const edges = await db .select({ - documentId: knowledgeEdge.fromRef, - entityId: knowledgeEdge.toRef, + documentId: memoryEdge.fromRef, + entityId: memoryEdge.toRef, }) - .from(knowledgeEdge) + .from(memoryEdge) .where( and( - eq(knowledgeEdge.tenantId, tenantId), - eq(knowledgeEdge.fromType, "document"), - eq(knowledgeEdge.toType, "entity"), - inArray(knowledgeEdge.fromRef, documentIds), + eq(memoryEdge.tenantId, tenantId), + eq(memoryEdge.fromType, "document"), + eq(memoryEdge.toType, "entity"), + inArray(memoryEdge.fromRef, documentIds), ), ); const map = new Map(); @@ -347,13 +347,13 @@ export async function fetchLexicalCandidates( } = params; const conditions = [ - eq(knowledgeChunk.tenantId, tenantId), - eq(knowledgeVersion.status, "active"), - eq(knowledgeVersion.generation, generation), + eq(memoryChunk.tenantId, tenantId), + eq(memoryVersion.status, "active"), + eq(memoryVersion.generation, generation), ]; if (kinds && kinds.length > 0) { - conditions.push(inArray(knowledgeDocument.kind, kinds)); + conditions.push(inArray(memoryDocument.kind, kinds)); } let rankExpr = sql`0::double precision`; @@ -361,56 +361,56 @@ export async function fetchLexicalCandidates( // Bound as a parameter and cast to regconfig — never spliced — and // required to match the language the generated column was built with // (verified against the catalog by runMemoryMigrations). - rankExpr = sql`ts_rank("knowledge"."chunk"."text_fts", plainto_tsquery(${ftsLanguage}::regconfig, ${query}))`; + rankExpr = sql`ts_rank("memory"."chunk"."text_fts", plainto_tsquery(${ftsLanguage}::regconfig, ${query}))`; conditions.push( - sql`"knowledge"."chunk"."text_fts" @@ plainto_tsquery(${ftsLanguage}::regconfig, ${query})`, + sql`"memory"."chunk"."text_fts" @@ plainto_tsquery(${ftsLanguage}::regconfig, ${query})`, ); } if (entityIds && entityIds.length > 0) { const matchingDocIds = db - .select({ documentId: knowledgeEdge.fromRef }) - .from(knowledgeEdge) + .select({ documentId: memoryEdge.fromRef }) + .from(memoryEdge) .where( and( - eq(knowledgeEdge.tenantId, tenantId), - eq(knowledgeEdge.fromType, "document"), - eq(knowledgeEdge.toType, "entity"), - inArray(knowledgeEdge.toRef, entityIds), + eq(memoryEdge.tenantId, tenantId), + eq(memoryEdge.fromType, "document"), + eq(memoryEdge.toType, "entity"), + inArray(memoryEdge.toRef, entityIds), ), ); - conditions.push(inArray(knowledgeDocument.id, matchingDocIds)); + conditions.push(inArray(memoryDocument.id, matchingDocIds)); } const rows = await db .select({ - chunkId: knowledgeChunk.id, - documentId: knowledgeChunk.documentId, - versionId: knowledgeChunk.versionId, - version: knowledgeVersion.version, - status: knowledgeVersion.status, - title: knowledgeDocument.title, - kind: knowledgeDocument.kind, - adapter: knowledgeDocument.adapter, - externalRef: knowledgeDocument.externalRef, - createdByKind: knowledgeVersion.createdByKind, - generatorAgentId: knowledgeVersion.generatorAgentId, - snippetText: knowledgeChunk.text, + chunkId: memoryChunk.id, + documentId: memoryChunk.documentId, + versionId: memoryChunk.versionId, + version: memoryVersion.version, + status: memoryVersion.status, + title: memoryDocument.title, + kind: memoryDocument.kind, + adapter: memoryDocument.adapter, + externalRef: memoryDocument.externalRef, + createdByKind: memoryVersion.createdByKind, + generatorAgentId: memoryVersion.generatorAgentId, + snippetText: memoryChunk.text, rank: rankExpr, - occurredAt: knowledgeVersion.occurredAt, - authority: knowledgeVersion.authority, + occurredAt: memoryVersion.occurredAt, + authority: memoryVersion.authority, }) - .from(knowledgeChunk) + .from(memoryChunk) .innerJoin( - knowledgeVersion, - eq(knowledgeChunk.versionId, knowledgeVersion.id), + memoryVersion, + eq(memoryChunk.versionId, memoryVersion.id), ) .innerJoin( - knowledgeDocument, - eq(knowledgeChunk.documentId, knowledgeDocument.id), + memoryDocument, + eq(memoryChunk.documentId, memoryDocument.id), ) .where(and(...conditions)) - .orderBy(desc(rankExpr), desc(knowledgeVersion.occurredAt)) + .orderBy(desc(rankExpr), desc(memoryVersion.occurredAt)) .limit(overfetchLimit); return rows as CandidateRow[]; @@ -453,7 +453,7 @@ export function hnswEfSearch(overfetchLimit: number): number { // Dense channel: embeds the query, then runs an ANN cosine-distance query // against the tenant's ACTIVE embedding table only (never a superseded or -// inactive model's table), joined back to knowledge_chunk/version/document. +// inactive model's table), joined back to memory_chunk/version/document. // Tenant + status + generation only — document access is post-filtered via // grant tags (same as the lexical channel). // Returns `null` (not an error) when there is no active embed model @@ -516,7 +516,7 @@ export async function fetchDenseCandidates( if (entityIds && entityIds.length > 0) { params.push(entityIds); entityClause = `AND kd.id IN ( - SELECT ke.from_ref FROM knowledge_edge ke + SELECT ke.from_ref FROM memory_edge ke WHERE ke.tenant_id = $1 AND ke.from_type = 'document' AND ke.to_type = 'entity' AND ke.to_ref = ANY($${params.length}::text[]) )`; @@ -529,9 +529,9 @@ export async function fetchDenseCandidates( kv.created_by_kind AS created_by_kind, kv.generator_agent_id AS generator_agent_id, c.text AS snippet_text, kv.occurred_at AS occurred_at, kv.authority AS authority FROM ${activeTable.tableName} e - JOIN "knowledge"."chunk" c ON c.id = e.chunk_id - JOIN "knowledge"."version" kv ON kv.id = c.version_id - JOIN "knowledge"."document" kd ON kd.id = c.document_id + JOIN "memory"."chunk" c ON c.id = e.chunk_id + JOIN "memory"."version" kv ON kv.id = c.version_id + JOIN "memory"."document" kd ON kd.id = c.document_id WHERE e.tenant_id = $1 AND c.tenant_id = $1 AND kv.status = 'active' AND kv.generation = ${generationParam} ${kindClause} diff --git a/src/services/timeline.ts b/src/services/timeline.ts index d88c2c7..4788203 100644 --- a/src/services/timeline.ts +++ b/src/services/timeline.ts @@ -12,7 +12,7 @@ import type { ConditionRegistry, GrantStore } from "@intx/authz"; import { canAccessDocument } from "../grant-tags.ts"; import type { Db } from "../db/client.ts"; -import { knowledgeDocument, knowledgeVersion } from "../db/schema.ts"; +import { memoryDocument, memoryVersion } from "../db/schema.ts"; import { log } from "../log.ts"; export type TimelineEvent = { @@ -52,7 +52,7 @@ const MAX_LIMIT = 100; * Tenant-only WHERE — document access is applied in application code. */ export function timelineWhere(tenantId: string) { - return eq(knowledgeDocument.tenantId, tenantId); + return eq(memoryDocument.tenantId, tenantId); } /** @@ -120,24 +120,24 @@ export async function listTimelineEvents( const rows = await params.db .select({ - documentId: knowledgeDocument.id, - title: knowledgeDocument.title, - adapter: knowledgeDocument.adapter, - externalRef: knowledgeDocument.externalRef, - occurredAt: knowledgeVersion.occurredAt, - createdByPrincipalId: knowledgeVersion.createdByPrincipalId, - accessTags: knowledgeDocument.accessTags, + documentId: memoryDocument.id, + title: memoryDocument.title, + adapter: memoryDocument.adapter, + externalRef: memoryDocument.externalRef, + occurredAt: memoryVersion.occurredAt, + createdByPrincipalId: memoryVersion.createdByPrincipalId, + accessTags: memoryDocument.accessTags, }) - .from(knowledgeDocument) + .from(memoryDocument) .innerJoin( - knowledgeVersion, + memoryVersion, and( - eq(knowledgeVersion.documentId, knowledgeDocument.id), - eq(knowledgeVersion.status, "active"), + eq(memoryVersion.documentId, memoryDocument.id), + eq(memoryVersion.status, "active"), ), ) .where(timelineWhere(params.tenantId)) - .orderBy(desc(knowledgeVersion.occurredAt)) + .orderBy(desc(memoryVersion.occurredAt)) .limit(fetchLimit); const { events, withheld } = await filterTimelineRows(rows, { diff --git a/src/services/transform.ts b/src/services/transform.ts index b700241..39fea77 100644 --- a/src/services/transform.ts +++ b/src/services/transform.ts @@ -185,7 +185,7 @@ function buildChunker(params: TransformConfigParams["chunk"]): Chunker { // common replays — a new model, a different endpoint, a context-window / // chunking change, any tuning tweak — name only what changes. An embed // endpoint is just a URL + capability options, trusted the same as the -// engine's own embed endpoint and KNOWLEDGE_DATABASE_URL — self-hosted or managed makes +// engine's own embed endpoint and DATABASE_URL — self-hosted or managed makes // no difference. function buildEmbedClientConfig( params: TransformConfigParams["embed"],