---
name: nightowls-knowledge
description: Use when giving a Night Owls agent retrieval over your documents, ingesting or deleting knowledge sources, or upgrading @nightowlsdev/knowledge past 0.1.x.
license: MIT
metadata:
  source: https://nightowls.dev/skills/nightowls-knowledge.md
  verified: "2026-07-26"
---

# The knowledge plane

Ships in `@nightowlsdev/knowledge@^0.2.0`.

## Read this before upgrading from 0.1.x

Migration `0001`'s unique key omitted `source_type`. Ingesting a `page` whose id collided with an
existing `guide` overwrote the guide's text. If you have ingested more than one source type, assume
the index may already be corrupt and re-ingest after migrating.

```ts
import { knowledgeMigrations } from "@nightowlsdev/knowledge";
for (const m of knowledgeMigrations({ dimensions: 1536 })) await apply(m.sql);
```

The store fix is hard-coupled to `0002` and cannot be made tolerant: a conflict target requires a
matching unique index, so there is no version of it that runs against the old schema. It fails
loudly instead of writing to the wrong key.

If you wrote your own regression test for this, check what it asserts. The pre-existing test counted
rows of one type and the collision produces such a row, so it passed identically before and after
the fix. Assert on chunk text.

`updated_at` backfills to `created_at`, so existing rows report their first ingest rather than a
fabricated now.

## Ingest and search

```ts
const store = createKnowledgeStore({ dbUrl, embedder });
await store.ingest({ tenantId, sourceType: "guide", sourceId: "onboarding", text });

const editor = defineAgent({ slug: "editor", skills: [searchKnowledgeTool(store)] });
```

The store also exposes `listSources`, `getSource`, `deleteSource`, `stats`, `pruneOldVersions`.

## Serve it

```ts
const handlers = createKnowledgeHandlers({
  store,
  auth: async (req) => resolveSession(req),
  authorizeWrite: async (req, session) => (isAdmin(session) ? { actor: session.actor } : null),
  onDelete: (info) => audit.record(info),
});
// { search, sources, source, stats, deleteSource }
```

`auth` is mandatory. Null or a throw is a 401 and there is no unauthenticated path.

`authorizeWrite` is authorization, separate and default-deny: omit it and `deleteSource` always
returns 403. It is a predicate returning `{ actor } | null`, not a boolean, because a boolean can
express "this deployment permits deletion" but never "this actor may delete this source", and
deletion here is unrecoverable.

## What will surprise you

`getSource` is a direct two-query lookup, not a filter over the paginated `listSources`. A lookup
that rides pagination can only find sources on the current page, which fails exactly when a library
is large enough for the feature to matter.

`onDelete` fires even when nothing matched. An attempted deletion is worth recording either way.

Search terms escape LIKE metacharacters, so a user-typed `%` is a literal percent rather than a
full-table scan.
