> Complete content index: https://sirannon.sondelali.com/llms.txt

# A database per AI agent

> Give every AI agent a database file of its own, decide where the boundary falls when you build the identifier, let the registry open each file on the first question and close it once the agent goes quiet, and delete every file a departing customer's agents wrote.

URL: https://sirannon.sondelali.com/docs/agent-databases
Section: AI agents
Version: 0.3 (@delali/sirannon-db@0.3.0, latest)

Sirannon opens a SQLite database as a file on a disk you control, so you can give every AI agent a database of its own. In the example below, a support desk gives each customer several agents, and each of those agents reads and writes one file. The registry opens that file the first time your code asks for the agent. It closes the file once the agent goes quiet, and the next question opens it again.

A support agent that works for two customers answers questions from both of them, and the two contracts set different refund windows. Where one table holds both agreements, only the `WHERE` clause in each query keeps them apart. The authors of a benchmark published in July 2026 built 85 scenarios, each holding four to ten users, and measured how much of one user's information each agent design let another user reach. Where every user talked to a single agent over one store, they measured a visibility-violation rate of 100% on each of the three models they tested, because each of those users reached whatever that agent could see ([arXiv 2607.05318](https://arxiv.org/abs/2607.05318)). With one file for each agent, the boundary is the file path, and your own code sets that path before the model runs.

## Give every customer's agent its own file

`createTenantResolver` maps an identifier onto a file under the directory you name, which turns `sundara-logistics__support-agent` into `data/tenants/sundara-logistics__support-agent.db`. Sirannon checks that identifier itself, accepting one that opens on a letter or a digit and holds nothing beyond letters, digits, hyphens, and underscores. An identifier such as `sundara-logistics/../kestrel-analytics__support-agent` therefore yields no path, and the registry opens nothing for it.

`agentId` is the one line of your own that touches the identifier, and it joins the customer to the agent's name. Pass the resolver to the registry as `lifecycle.autoOpen.resolver`. The registry then calls it the first time your code resolves an agent, opens the file at the path it returns, and applies the `migrations` list to that file. The [migrations guide](/docs/migrations) covers what the registry does when a later version reaches a file that is already on disk, and [configuration](/docs/configuration) lists every option the resolver accepts.

`brainTools` takes the agent identifier as an argument, which leaves the `recall` tool the model calls already holding one identifier, with the model supplying only the subject. Swap `mockModel` for a model string when you want a real model to choose the calls, since the mock is here so that the panels below print the same text on every run.

<CodeGroup defaultTab="agents.ts">

```ts title="agents.ts"

const AGREED_ON = Date.UTC(2026, 3, 9)

resetTenantRoot()

const agreeRefundWindow = async (customer: string, days: string): Promise<void> => {
  await remember(sirannon, agentId(customer, 'support-agent'), {
    subject: 'refund window',
    predicate: 'length',
    value: days,
    source: 'signed-contract',
    writtenBy: 'contract-loader',
    learnedAt: AGREED_ON,
  })
}

const askSupportAgent = async (customer: string): Promise<string> => {
  const agent = new ToolLoopAgent({
    model: mockModel('refund window'),
    instructions: 'Answer from this customer store, using the recall tool.',
    tools: brainTools(sirannon, agentId(customer, 'support-agent')),
  })

  const reply = await agent.generate({ prompt: 'How long is the refund window?' })
  return reply.text
}

await agreeRefundWindow('sundara-logistics', '30 days')
await agreeRefundWindow('kestrel-analytics', '14 days')

await remember(sirannon, agentId('sundara-logistics', 'billing-agent'), {
  subject: 'invoice currency',
  predicate: 'code',
  value: 'USD',
  source: 'signed-contract',
  writtenBy: 'contract-loader',
  learnedAt: AGREED_ON,
})

const databaseFiles = async (): Promise<string[]> => {
  const entries = await readdir(TENANT_ROOT)
  return entries.filter((name) => name.endsWith('.db')).sort()
}

console.log(
  JSON.stringify(
    {
      sundaraSupportAnswer: await askSupportAgent('sundara-logistics'),
      kestrelSupportAnswer: await askSupportAgent('kestrel-analytics'),
      rejectedIdentifier: resolveAgentFile('sundara-logistics/../kestrel-analytics__support-agent') ?? null,
      filesOnDisk: await databaseFiles(),
    },
    null,
    2
  )
)

await sirannon.shutdown()
```

```json result open
{
  "sundaraSupportAnswer": "The refund window is 30 days.",
  "kestrelSupportAnswer": "The refund window is 14 days.",
  "rejectedIdentifier": null,
  "filesOnDisk": [
    "kestrel-analytics__support-agent.db",
    "sundara-logistics__billing-agent.db",
    "sundara-logistics__support-agent.db"
  ]
}
```

```ts title="agent-store.ts"

export const TENANT_ROOT = './data/tenants'

export const agentId = (customer: string, agent: string): string => `${customer}__${agent}`

export const resolveAgentFile = createTenantResolver({ basePath: TENANT_ROOT })

export const sirannon = new Sirannon({
  driver: betterSqlite3(),
  migrations: [
    {
      version: 1,
      name: 'fact',
      up: `CREATE TABLE fact (
        id INTEGER PRIMARY KEY,
        subject TEXT NOT NULL,
        predicate TEXT NOT NULL,
        value TEXT NOT NULL,
        source TEXT NOT NULL,
        written_by TEXT NOT NULL,
        learned_at INTEGER NOT NULL,
        expires_at INTEGER,
        superseded_by INTEGER REFERENCES fact(id)
      )`,
    },
  ],
  lifecycle: {
    autoOpen: { resolver: resolveAgentFile },
  },
})

export const resetTenantRoot = (): void => {
  rmSync(TENANT_ROOT, { recursive: true, force: true })
  mkdirSync(TENANT_ROOT, { recursive: true })
}
```

```ts title="memory-tools.ts"

export interface Belief {
  predicate: string
  value: string
}

export interface LearnedFact {
  subject: string
  predicate: string
  value: string
  source: string
  writtenBy: string
  learnedAt: number
}

export const remember = async (registry: Sirannon, agentId: string, fact: LearnedFact): Promise<void> => {
  const db = await registry.resolve(agentId)
  if (!db) throw new Error(`The resolver names no file for ${agentId}`)
  await db.execute(
    `INSERT INTO fact (subject, predicate, value, source, written_by, learned_at)
     VALUES (?, ?, ?, ?, ?, ?)`,
    [fact.subject, fact.predicate, fact.value, fact.source, fact.writtenBy, fact.learnedAt]
  )
}

export const recall = async (registry: Sirannon, agentId: string, subject: string): Promise<Belief[]> => {
  const db = await registry.resolve(agentId)
  if (!db) return []
  return db.query<Belief>('SELECT predicate, value FROM fact WHERE subject = ? ORDER BY id', [subject])
}

export const brainTools = (registry: Sirannon, agentId: string) => ({
  recall: tool({
    description: 'Look up what this customer contract sets for a subject.',
    inputSchema: z.object({ subject: z.string() }),
    execute: async ({ subject }: { subject: string }): Promise<Belief[]> => recall(registry, agentId, subject),
  }),
})
```

```ts title="mock-model.ts"

const NO_USAGE = {
  inputTokens: { total: 0, noCache: 0, cacheRead: undefined, cacheWrite: undefined },
  outputTokens: { total: 0, text: 0, reasoning: undefined },
}

interface ReturnedRow {
  value: string
}

interface ToolMessage {
  role: string
  content: { output?: { value?: unknown } }[]
}

const rowsReturnedSoFar = (prompt: unknown): ReturnedRow[] | undefined => {
  for (const message of prompt as ToolMessage[]) {
    if (message.role !== 'tool') continue
    for (const part of message.content) {
      if (Array.isArray(part.output?.value)) return part.output.value as ReturnedRow[]
    }
  }
  return undefined
}

export const mockModel = (subject: string) =>
  new MockLanguageModelV4({
    doGenerate: async ({ prompt }) => {
      const rows = rowsReturnedSoFar(prompt)

      if (rows === undefined) {
        return {
          content: [
            {
              type: 'tool-call' as const,
              toolCallId: 'recall-1',
              toolName: 'recall',
              input: JSON.stringify({ subject }),
            },
          ],
          finishReason: { unified: 'tool-calls' as const, raw: undefined },
          usage: NO_USAGE,
          warnings: [],
        }
      }

      const first = rows[0]
      const text = first === undefined ? `I have no record of the ${subject}.` : `The ${subject} is ${first.value}.`

      return {
        content: [{ type: 'text' as const, text }],
        finishReason: { unified: 'stop' as const, raw: undefined },
        usage: NO_USAGE,
        warnings: [],
      }
    },
  })
```

</CodeGroup>

Each agent answers with its own customer's refund window. The resolver returns no path for the identifier holding `..`, and the registry therefore opens no database for that one. Sundara Logistics has two files and Kestrel Analytics has one.

## Decide where the boundary falls

The identifier is what puts two agents in one store or in two, because the resolver turns it into a path and every query then runs against that one file. `agentId` joins the customer to the role, so a customer's support agent and its billing agent hold separate files. An identifier without the role would give a whole customer one store, and an identifier holding the session would give each conversation a file of its own.

The questions below put that boundary to the test. Sundara Logistics agreed an invoice currency with its billing agent, and the example then puts that same question to the support agent serving the same customer.

<CodeGroup defaultTab="agents.ts">

```ts title="agents.ts"

const AGREED_ON = Date.UTC(2026, 3, 9)

resetTenantRoot()

const agreeRefundWindow = async (customer: string, days: string): Promise<void> => {
  await remember(sirannon, agentId(customer, 'support-agent'), {
    subject: 'refund window',
    predicate: 'length',
    value: days,
    source: 'signed-contract',
    writtenBy: 'contract-loader',
    learnedAt: AGREED_ON,
  })
}

const askSupportAgent = async (customer: string): Promise<string> => { // [!code --]
const ask = async (customer: string, role: string, subject: string, question: string): Promise<string> => { // [!code ++]
  const agent = new ToolLoopAgent({
// [!code --:3]
    model: mockModel('refund window'),
    instructions: 'Answer from this customer store, using the recall tool.',
    tools: brainTools(sirannon, agentId(customer, 'support-agent')),
// [!code ++:3]
    model: mockModel(subject),
    instructions: 'Answer from the store this agent holds, using the recall tool.',
    tools: brainTools(sirannon, agentId(customer, role)),
  })

  const reply = await agent.generate({ prompt: 'How long is the refund window?' }) // [!code --]
  const reply = await agent.generate({ prompt: question }) // [!code ++]
  return reply.text
}
// [!code ++:6]

const askSupportAgent = (customer: string): Promise<string> =>
  ask(customer, 'support-agent', 'refund window', 'How long is the refund window?')

const askForTheCurrency = (customer: string, role: string): Promise<string> =>
  ask(customer, role, 'invoice currency', 'Which currency do we invoice in?')

await agreeRefundWindow('sundara-logistics', '30 days')
await agreeRefundWindow('kestrel-analytics', '14 days')

await remember(sirannon, agentId('sundara-logistics', 'billing-agent'), {
  subject: 'invoice currency',
  predicate: 'code',
  value: 'USD',
  source: 'signed-contract',
  writtenBy: 'contract-loader',
  learnedAt: AGREED_ON,
})

const databaseFiles = async (): Promise<string[]> => {
  const entries = await readdir(TENANT_ROOT)
  return entries.filter((name) => name.endsWith('.db')).sort()
}

console.log(
  JSON.stringify(
    {
      sundaraSupportAnswer: await askSupportAgent('sundara-logistics'),
      kestrelSupportAnswer: await askSupportAgent('kestrel-analytics'),
// [!code ++:2]
      sundaraBillingOnTheCurrency: await askForTheCurrency('sundara-logistics', 'billing-agent'),
      sundaraSupportOnTheCurrency: await askForTheCurrency('sundara-logistics', 'support-agent'),
      rejectedIdentifier: resolveAgentFile('sundara-logistics/../kestrel-analytics__support-agent') ?? null,
      filesOnDisk: await databaseFiles(),
    },
    null,
    2
  )
)

await sirannon.shutdown()
```

```json result open
{
  "sundaraSupportAnswer": "The refund window is 30 days.",
  "kestrelSupportAnswer": "The refund window is 14 days.",
  "sundaraBillingOnTheCurrency": "The invoice currency is USD.",
  "sundaraSupportOnTheCurrency": "I have no record of the invoice currency.",
  "rejectedIdentifier": null,
  "filesOnDisk": [
    "kestrel-analytics__support-agent.db",
    "sundara-logistics__billing-agent.db",
    "sundara-logistics__support-agent.db"
  ]
}
```

</CodeGroup>

The billing agent answers with the currency its own file holds, and the support agent for that same customer has no record of it. Neither query names a customer or a role, since the file each one runs against has already settled both.

## Hold only a few of those files open at once

A fleet can hold more agents than your process has file handles for. `maxOpen` therefore bounds how many of these databases stay open together, while `idleTimeout` closes any database that goes without a read or a write for that long. Set both in the `lifecycle` block you already pass the resolver to. Where a resolve reaches the registry at its cap, it closes the least recently used database to make room, and it raises `MAX_DATABASES` when the count is still at the cap after that close.

The cap below is two and the timeout is one second, which is enough for a single run to reach both. Take your own cap from the file descriptors your process may hold along with the number of agents you expect to work at the same time, and set the timeout in minutes.

`onDatabaseClose` reports each close as the registry makes it. The file below records those closes, and it then waits on that hook until the registry holds nothing open. That wait is here so that one file can show the sweep, and your own service would register the same hook and act on each close as it arrives. The [lifecycle guide](/docs/hooks-metrics-and-lifecycle) covers the same two options for tenants that are not agents.

<AgentFleetDiagram
  filesOnDisk={3}
  maxOpen={2}
  moments={[
    { label: 'Open while the agents answer', open: 2 },
    { label: 'Open once the idle timeout passes', open: 0 },
    { label: 'Open after the next question', open: 1 },
  ]}
  caption="Three agents write three files, and the registry holds at most two of them open at any moment. Every figure comes from the run below."
/>

<CodeGroup defaultTab="agents.ts">

```ts title="agents.ts"

const AGREED_ON = Date.UTC(2026, 3, 9)

resetTenantRoot()

const agreeRefundWindow = async (customer: string, days: string): Promise<void> => {
  await remember(sirannon, agentId(customer, 'support-agent'), {
    subject: 'refund window',
    predicate: 'length',
    value: days,
    source: 'signed-contract',
    writtenBy: 'contract-loader',
    learnedAt: AGREED_ON,
  })
}

const ask = async (customer: string, role: string, subject: string, question: string): Promise<string> => {
  const agent = new ToolLoopAgent({
    model: mockModel(subject),
    instructions: 'Answer from the store this agent holds, using the recall tool.',
    tools: brainTools(sirannon, agentId(customer, role)),
  })

  const reply = await agent.generate({ prompt: question })
  return reply.text
}

const askSupportAgent = (customer: string): Promise<string> =>
  ask(customer, 'support-agent', 'refund window', 'How long is the refund window?')

const askForTheCurrency = (customer: string, role: string): Promise<string> =>
  ask(customer, role, 'invoice currency', 'Which currency do we invoice in?')

await agreeRefundWindow('sundara-logistics', '30 days')
await agreeRefundWindow('kestrel-analytics', '14 days')

await remember(sirannon, agentId('sundara-logistics', 'billing-agent'), {
  subject: 'invoice currency',
  predicate: 'code',
  value: 'USD',
  source: 'signed-contract',
  writtenBy: 'contract-loader',
  learnedAt: AGREED_ON,
})

const databaseFiles = async (): Promise<string[]> => {
  const entries = await readdir(TENANT_ROOT)
  return entries.filter((name) => name.endsWith('.db')).sort()
}

// [!code ++:35]
const closedByTheRegistry: string[] = []

sirannon.onDatabaseClose(({ databaseId }) => {
  closedByTheRegistry.push(databaseId)
})

const untilTheRegistryClosesEveryDatabase = (): Promise<void> =>
  new Promise((settle, fail) => {
    const giveUp = setTimeout(() => {
      fail(new Error('the idle sweep closed no database'))
    }, 10_000)

    sirannon.onDatabaseClose(() => {
      if (sirannon.databases().size > 0) return
      clearTimeout(giveUp)
      settle()
    })
  })

const sundaraSupportAnswer = await askSupportAgent('sundara-logistics')
const kestrelSupportAnswer = await askSupportAgent('kestrel-analytics')
const sundaraBillingOnTheCurrency = await askForTheCurrency('sundara-logistics', 'billing-agent')
const sundaraSupportOnTheCurrency = await askForTheCurrency('sundara-logistics', 'support-agent')

const filesOnDisk = await databaseFiles()
const openAfterTheQuestions = sirannon.databases().size
const closedByTheCap = [...closedByTheRegistry]

await untilTheRegistryClosesEveryDatabase()
const closedByTheIdleSweep = closedByTheRegistry.slice(closedByTheCap.length)
const openAfterTheIdleSweep = sirannon.databases().size

const sundaraAnswerAfterTheSweep = await askSupportAgent('sundara-logistics')
const openAfterThatQuestion = sirannon.databases().size

console.log(
  JSON.stringify(
    {
// [!code --:4]
      sundaraSupportAnswer: await askSupportAgent('sundara-logistics'),
      kestrelSupportAnswer: await askSupportAgent('kestrel-analytics'),
      sundaraBillingOnTheCurrency: await askForTheCurrency('sundara-logistics', 'billing-agent'),
      sundaraSupportOnTheCurrency: await askForTheCurrency('sundara-logistics', 'support-agent'),
// [!code ++:4]
      sundaraSupportAnswer,
      kestrelSupportAnswer,
      sundaraBillingOnTheCurrency,
      sundaraSupportOnTheCurrency,
      rejectedIdentifier: resolveAgentFile('sundara-logistics/../kestrel-analytics__support-agent') ?? null,
      filesOnDisk: await databaseFiles(), // [!code --]
// [!code ++:7]
      filesOnDisk,
      openAfterTheQuestions,
      closedByTheCap,
      closedByTheIdleSweep,
      openAfterTheIdleSweep,
      sundaraAnswerAfterTheSweep,
      openAfterThatQuestion,
    },
    null,
    2
  )
)

await sirannon.shutdown()
```

```json result open
{
  "sundaraSupportAnswer": "The refund window is 30 days.",
  "kestrelSupportAnswer": "The refund window is 14 days.",
  "sundaraBillingOnTheCurrency": "The invoice currency is USD.",
  "sundaraSupportOnTheCurrency": "I have no record of the invoice currency.",
  "rejectedIdentifier": null,
  "filesOnDisk": [
    "kestrel-analytics__support-agent.db",
    "sundara-logistics__billing-agent.db",
    "sundara-logistics__support-agent.db"
  ],
  "openAfterTheQuestions": 2,
  "closedByTheCap": [
    "kestrel-analytics__support-agent",
    "sundara-logistics__billing-agent",
    "sundara-logistics__support-agent",
    "kestrel-analytics__support-agent"
  ],
  "closedByTheIdleSweep": [
    "sundara-logistics__billing-agent",
    "sundara-logistics__support-agent"
  ],
  "openAfterTheIdleSweep": 0,
  "sundaraAnswerAfterTheSweep": "The refund window is 30 days.",
  "openAfterThatQuestion": 1
}
```

```ts title="agent-store.ts"

export const TENANT_ROOT = './data/tenants'

export const agentId = (customer: string, agent: string): string => `${customer}__${agent}`

export const resolveAgentFile = createTenantResolver({ basePath: TENANT_ROOT })

export const sirannon = new Sirannon({
  driver: betterSqlite3(),
  migrations: [
    {
      version: 1,
      name: 'fact',
      up: `CREATE TABLE fact (
        id INTEGER PRIMARY KEY,
        subject TEXT NOT NULL,
        predicate TEXT NOT NULL,
        value TEXT NOT NULL,
        source TEXT NOT NULL,
        written_by TEXT NOT NULL,
        learned_at INTEGER NOT NULL,
        expires_at INTEGER,
        superseded_by INTEGER REFERENCES fact(id)
      )`,
    },
  ],
  lifecycle: {
    autoOpen: { resolver: resolveAgentFile },
// [!code ++:2]
    idleTimeout: 1_000,
    maxOpen: 2,
  },
})

export const resetTenantRoot = (): void => {
  rmSync(TENANT_ROOT, { recursive: true, force: true })
  mkdirSync(TENANT_ROOT, { recursive: true })
}
```

</CodeGroup>

Three agents write three files, and the cap holds the registry to two open handles, which is why it closes four databases along the way to keep room for the next one. The idle sweep then closes the last two, and `onDatabaseClose` firing for the second of those releases the example from its wait. The question after that opens one of those files again and the agent gives the same answer, because closing a database leaves the file where it is.

## Delete every file a customer's agents wrote

A customer who leaves is done with their agents, and the files those agents wrote are therefore yours to delete. `forgetCustomer` closes every open database whose identifier starts with that customer, since SQLite holds each file open until its close returns. It then rebuilds each path with `tenantPath` and removes the database file along with its write-ahead log and its shared-memory file, because a process killed part-way through leaves all three of them behind.

<CodeGroup defaultTab="agents.ts">

```ts title="agents.ts"

import { brainTools, remember } from './memory-tools' // [!code --]
import { brainTools, forgetCustomer, remember } from './memory-tools' // [!code ++]

const AGREED_ON = Date.UTC(2026, 3, 9)

resetTenantRoot()

const agreeRefundWindow = async (customer: string, days: string): Promise<void> => {
  await remember(sirannon, agentId(customer, 'support-agent'), {
    subject: 'refund window',
    predicate: 'length',
    value: days,
    source: 'signed-contract',
    writtenBy: 'contract-loader',
    learnedAt: AGREED_ON,
  })
}

const ask = async (customer: string, role: string, subject: string, question: string): Promise<string> => {
  const agent = new ToolLoopAgent({
    model: mockModel(subject),
    instructions: 'Answer from the store this agent holds, using the recall tool.',
    tools: brainTools(sirannon, agentId(customer, role)),
  })

  const reply = await agent.generate({ prompt: question })
  return reply.text
}

const askSupportAgent = (customer: string): Promise<string> =>
  ask(customer, 'support-agent', 'refund window', 'How long is the refund window?')

const askForTheCurrency = (customer: string, role: string): Promise<string> =>
  ask(customer, role, 'invoice currency', 'Which currency do we invoice in?')

await agreeRefundWindow('sundara-logistics', '30 days')
await agreeRefundWindow('kestrel-analytics', '14 days')

await remember(sirannon, agentId('sundara-logistics', 'billing-agent'), {
  subject: 'invoice currency',
  predicate: 'code',
  value: 'USD',
  source: 'signed-contract',
  writtenBy: 'contract-loader',
  learnedAt: AGREED_ON,
})

const databaseFiles = async (): Promise<string[]> => {
  const entries = await readdir(TENANT_ROOT)
  return entries.filter((name) => name.endsWith('.db')).sort()
}

const closedByTheRegistry: string[] = []

sirannon.onDatabaseClose(({ databaseId }) => {
  closedByTheRegistry.push(databaseId)
})

const untilTheRegistryClosesEveryDatabase = (): Promise<void> =>
  new Promise((settle, fail) => {
    const giveUp = setTimeout(() => {
      fail(new Error('the idle sweep closed no database'))
    }, 10_000)

    sirannon.onDatabaseClose(() => {
      if (sirannon.databases().size > 0) return
      clearTimeout(giveUp)
      settle()
    })
  })

const sundaraSupportAnswer = await askSupportAgent('sundara-logistics')
const kestrelSupportAnswer = await askSupportAgent('kestrel-analytics')
const sundaraBillingOnTheCurrency = await askForTheCurrency('sundara-logistics', 'billing-agent')
const sundaraSupportOnTheCurrency = await askForTheCurrency('sundara-logistics', 'support-agent')

const filesOnDisk = await databaseFiles()
const openAfterTheQuestions = sirannon.databases().size
const closedByTheCap = [...closedByTheRegistry]

await untilTheRegistryClosesEveryDatabase()
const closedByTheIdleSweep = closedByTheRegistry.slice(closedByTheCap.length)
const openAfterTheIdleSweep = sirannon.databases().size

const sundaraAnswerAfterTheSweep = await askSupportAgent('sundara-logistics')
const openAfterThatQuestion = sirannon.databases().size

// [!code ++:5]
const removedForSundara = await forgetCustomer(sirannon, 'sundara-logistics')
const filesAfterTheDelete = await databaseFiles()
const sundaraAnswerAfterTheDelete = await askSupportAgent('sundara-logistics')
const kestrelAnswerAfterTheDelete = await askSupportAgent('kestrel-analytics')

console.log(
  JSON.stringify(
    {
      sundaraSupportAnswer,
      kestrelSupportAnswer,
      sundaraBillingOnTheCurrency,
      sundaraSupportOnTheCurrency,
      rejectedIdentifier: resolveAgentFile('sundara-logistics/../kestrel-analytics__support-agent') ?? null,
      filesOnDisk,
      openAfterTheQuestions,
      closedByTheCap,
      closedByTheIdleSweep,
      openAfterTheIdleSweep,
      sundaraAnswerAfterTheSweep,
      openAfterThatQuestion,
// [!code ++:4]
      removedForSundara,
      filesAfterTheDelete,
      sundaraAnswerAfterTheDelete,
      kestrelAnswerAfterTheDelete,
    },
    null,
    2
  )
)

await sirannon.shutdown()
```

```json result open
{
  "sundaraSupportAnswer": "The refund window is 30 days.",
  "kestrelSupportAnswer": "The refund window is 14 days.",
  "sundaraBillingOnTheCurrency": "The invoice currency is USD.",
  "sundaraSupportOnTheCurrency": "I have no record of the invoice currency.",
  "rejectedIdentifier": null,
  "filesOnDisk": [
    "kestrel-analytics__support-agent.db",
    "sundara-logistics__billing-agent.db",
    "sundara-logistics__support-agent.db"
  ],
  "openAfterTheQuestions": 2,
  "closedByTheCap": [
    "kestrel-analytics__support-agent",
    "sundara-logistics__billing-agent",
    "sundara-logistics__support-agent",
    "kestrel-analytics__support-agent"
  ],
  "closedByTheIdleSweep": [
    "sundara-logistics__billing-agent",
    "sundara-logistics__support-agent"
  ],
  "openAfterTheIdleSweep": 0,
  "sundaraAnswerAfterTheSweep": "The refund window is 30 days.",
  "openAfterThatQuestion": 1,
  "removedForSundara": 2,
  "filesAfterTheDelete": [
    "kestrel-analytics__support-agent.db"
  ],
  "sundaraAnswerAfterTheDelete": "I have no record of the refund window.",
  "kestrelAnswerAfterTheDelete": "The refund window is 14 days."
}
```

```ts title="memory-tools.ts"
import type { Sirannon } from '@delali/sirannon-db' // [!code --]
// [!code ++:2]

import { TENANT_ROOT } from './agent-store' // [!code ++]

export interface Belief {
  predicate: string
  value: string
}

export interface LearnedFact {
  subject: string
  predicate: string
  value: string
  source: string
  writtenBy: string
  learnedAt: number
}

export const remember = async (registry: Sirannon, agentId: string, fact: LearnedFact): Promise<void> => {
  const db = await registry.resolve(agentId)
  if (!db) throw new Error(`The resolver names no file for ${agentId}`)
  await db.execute(
    `INSERT INTO fact (subject, predicate, value, source, written_by, learned_at)
     VALUES (?, ?, ?, ?, ?, ?)`,
    [fact.subject, fact.predicate, fact.value, fact.source, fact.writtenBy, fact.learnedAt]
  )
}

export const recall = async (registry: Sirannon, agentId: string, subject: string): Promise<Belief[]> => {
  const db = await registry.resolve(agentId)
  if (!db) return []
  return db.query<Belief>('SELECT predicate, value FROM fact WHERE subject = ? ORDER BY id', [subject])
}

export const brainTools = (registry: Sirannon, agentId: string) => ({
  recall: tool({
    description: 'Look up what this customer contract sets for a subject.',
    inputSchema: z.object({ subject: z.string() }),
    execute: async ({ subject }: { subject: string }): Promise<Belief[]> => recall(registry, agentId, subject),
  }),
})
// [!code ++:18]

export const forgetCustomer = async (registry: Sirannon, customer: string): Promise<number> => {
  const prefix = `${customer}__`

  for (const id of [...registry.databases().keys()]) {
    if (id.startsWith(prefix)) await registry.close(id)
  }

  const files = (await readdir(TENANT_ROOT)).filter((name) => name.startsWith(prefix) && name.endsWith('.db'))
  for (const file of files) {
    const path = tenantPath(TENANT_ROOT, file.slice(0, -'.db'.length))
    for (const suffix of ['', '-wal', '-shm']) {
      await rm(`${path}${suffix}`, { force: true })
    }
  }

  return files.length
}
```

</CodeGroup>

The delete removes both of that customer's databases along with their write-ahead and shared-memory files. Kestrel Analytics answers as it did before, because `forgetCustomer` matches only the identifiers opening with the customer you pass it. The identifier stays valid, and the next question therefore opens an empty database and writes the file again. Deleting those files removes what a customer's agents stored, while refusing those agents a database at all is a decision your own code makes.

## Where to go next

[Memory and forgetting](/docs/agent-memory) covers the `fact` table in each of these files, where every row has a source and an expiry date, and a newer value marks the older row as superseded. Where a false fact reaches one of these files, [rewinding memory](/docs/agent-memory-rewind) restores that one database to the state before that write, leaving every other customer's files untouched. For a record of which agent wrote each row, read [auditing a fleet](/docs/agent-audit).
