Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ CI runs `typecheck` + `test` — both must pass before any push.

## Layout

- `src/index.ts` — public surface: `createMemory` (optional `app` registers HTTP), `registerMemoryRoutes`
- `src/index.ts` — public surface: `createMemory`, `createMemoryRoutes`

- `src/mount-config.ts` / `src/config.ts` — mount config + engine config
- `src/routes/` — Hono routes (`add`, `search`, `list`, `feed`, retention
Expand Down
13 changes: 7 additions & 6 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ The store was detachable from a larger backend, then mountable:
- Document access is Interchange grant tags on the row (`accessTags` + creator),
not a private ACL engine inside this package.

It ships as `createMemory({ app, … })`: the host passes its Hono app and grant
store; the library registers routes, reads identity from request context, and
talks to its DocumentStore. No second server.
It ships as `createMemory(…)` plus `createMemoryRoutes(deps)`: the host builds
the plane, mounts the returned Hono sub-app, and passes its `requireGrant`; the
routes read identity from request context and talk to the DocumentStore. No
second server.

## Product path

Expand Down Expand Up @@ -53,8 +54,8 @@ helpers are optional multi-writer / backfill — not the primary path.
puts `principal` + `tenant` on context; routes read identity from there
(`tenantId = principal.tenantId`, `principalId = principal.id`). A host
with a non-browser caller (e.g. a workflow-run child with its own sidecar
bearer token) may instead pass `callerResolver` (`RouteDeps` /
`createMemory`) — the host still does 100% of the authenticating, it just
bearer token) may instead pass `callerResolver` to `createMemoryRoutes`
— the host still does 100% of the authenticating, it just
hands the resolved `{ tenantId, principalId }` in through the seam instead
of setting context itself. Either way the resolved identity, never
anything from the request body, is what `grantGuard` authorizes.
Expand Down Expand Up @@ -96,7 +97,7 @@ exposes the same three verbs.

## Mounted surface

`createMemory({ app })` registers:
`createMemoryRoutes(deps)`, mounted at `/api/tenants/:tenantId/memory`, serves:

- `POST /api/tenants/:tenantId/memory/add` — ingest (raw + derive on the default store).
- `POST /api/tenants/:tenantId/memory/search` — hybrid retrieval (FTS + dense → RRF → rerank →
Expand Down
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `MEMORY_GRANT_REQUIREMENTS` is read from `package.json`
`interchange.grantRequirements`, now the only declaration.
`MEMORY_CAPABILITY_IDS` is typed `string[]`.
- `createMemoryRoutes({ memory, requireGrant, callerResolver? })` returns the
memory routes as a `Hono<TenantEnv>` sub-app with paths relative to its
mount point; hosts mount it at `/api/tenants/:tenantId/memory`. It replaces
`createMemory({ app })` and `registerMemoryRoutes`, and `RouteDeps` no
longer carries `grants`. `createMemory` only builds the plane.
- The package root exports only the public API. Internal services and
helpers (transform, retention, feed, share materialization, corroboration,
embed model registry, degrade metrics, FTS helpers), the test fakes, and
`resolveGrantConfig` are no longer exported. The distiller stays at
`@corbits/memory/distiller` and migrations at `@corbits/memory/migrations`.

### Added

Expand All @@ -29,7 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
in-process `Memory`. New `memory:forget` / `memory:purge` grant requirements
(`source: "creator"`) and `capabilityIdsForSurface()` so distiller/tools
installs no longer pick up routes-only capabilities by accident.
- `RouteDeps.callerResolver` / `createMemory({ callerResolver })` — an
- `RouteDeps.callerResolver` — an
optional host-supplied resolver from a request to a `{ tenantId,
principalId }` scope, for a caller that never goes through the host's
tenant-session middleware (e.g. a workflow-run child authenticating with
Expand Down
17 changes: 9 additions & 8 deletions IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ and wire shapes. For the "why standalone" / boundaries story, read

```
src/
index.ts # createMemory / registerMemoryRoutes / mountWorkflowMemory + distiller re-exports
index.ts # createMemory / createMemoryRoutes / mountWorkflowMemory

mount-config.ts # MemoryConfig + loadMemoryConfig() — the mount config
config.ts # EngineConfig — the core vector-plane config (db + embed + rerank)
Expand All @@ -23,7 +23,7 @@ src/
migrations.ts # runMemoryMigrations(url)
ports/ # DocumentStore / SourceProvider + fakes
routes/ # the mounted tenant routes
mount.ts # registerMemoryRoutes (HTTP)
mount.ts # createMemoryRoutes (HTTP sub-app)

deps.ts # RouteDeps, caller(c) (context identity), grantGuard
add.ts, search.ts, list.ts, feed.ts
Expand Down Expand Up @@ -605,16 +605,17 @@ end-user agents.

## Mounted routes

`createMemory({ app })` registers these onto the host app. Identity is the request
`createMemoryRoutes(deps)` serves these once the host mounts it at
`/api/tenants/:tenantId/memory`. Identity is the request

principal read off the Interchange context (`caller(c)` →
`{ scopeId: principal.tenantId, subjectId: principal.id }`); clients never send
`tenant_id`/`principal_id` — the handlers only read title/text/query/limit/access_tags/share.
Each route is guarded with `grantGuard(deps, action)`, which applies the host's
`requireGrant("memory", action)` when provided (else a pass-through).

**Machine callers (CL-6286):** `RouteDeps.callerResolver` /
`createMemory({ callerResolver })` lets a host resolve identity for a caller
**Machine callers (CL-6286):** `RouteDeps.callerResolver` (passed to
`createMemoryRoutes`) lets a host resolve identity for a caller
that never goes through its tenant-session middleware — e.g. a workflow-run
child authenticating with its own sidecar bearer token. Unset by default
(every existing host is unaffected). When set, `resolveCaller` (`deps.ts`)
Expand All @@ -640,7 +641,7 @@ surface, or a migrating host silently loses them.
| `POST /api/tenants/:tenantId/memory/documents/:documentId/purge` | `purge` | none | `200 { documentId, deleted, reason? }`; `403` unless caller is the document's creator; `404` unknown document. Hard-deletes the row — irreversible; refused while a `durable` version is untombstoned. |
| `POST /api/tenants/:tenantId/memory/versions/:versionId/retention-class` | `forget` | `{ retention_class }` | `200 { versionId, documentId, status }`; `400` invalid class; `403` unless caller is the version's creator; `404` unknown version. |

`registerMemoryRoutes` and `createMemory({ app })` register these seven HTTP
`createMemoryRoutes` serves these seven HTTP
routes (add, search, list, feed, forget, purge, retention-class).
**Capture** (`services/capture.ts`) is the write path inside `add`.
**Search** (`services/search.ts`) is hybrid retrieval. Deployed agents do
Expand All @@ -658,7 +659,7 @@ have HTTP to the tenant tree can use `createMemoryHttpClient`
### Run-scoped sidecar (`mountWorkflowMemory`)

`src/workflow-mount.ts`, re-exported from the barrel. Parallel to the
tenant tree — do not fold agent-bearer auth into `registerMemoryRoutes`.
tenant tree — do not fold agent-bearer auth into `createMemoryRoutes`.
`package.json` exports `@corbits/memory/sidecar-bundle` →
`src/sidecar-bundle.ts`.

Expand Down Expand Up @@ -713,7 +714,7 @@ store implements `WritableGrantStore.putGrant`:
1. Appends `memory.doc:<documentId>` to the document's `access_tags`.
2. Writes one allow/`search` grant per peer on that resource, origin
`system`, with `conditions.memoryShare` audit payload.
3. `resolveGrantConfig` merges `MEMORY_SHARE_CONDITION_REGISTRY` so those
3. `createMemory` merges `MEMORY_SHARE_CONDITION_REGISTRY` so those
condition keys are not fail-closed-skipped by `@intx/authz`.

Without a writable store: tags only + warn log (peers need host grants).
Expand Down
7 changes: 4 additions & 3 deletions PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ never creates one; it mounts onto yours.

| Surface | Role |
| --- | --- |
| `createMemory({ app, … })` | Register `/api/tenants/:tenantId/memory/*` + return the plane |
| `createMemory({ … })` | Build the plane |
| `createMemoryRoutes({ memory, requireGrant })` | Hono sub-app the host mounts at `/api/tenants/:tenantId/memory` |
| `mountWorkflowMemory(app, { memory, agentToken })` | Parallel run-scoped `/api/workflow-memory/*` for deployed agents |
| `loadMemoryConfig()` | Config from env |
| `runMemoryMigrations(url)` | Apply pgvector schema |
| `registerMemoryRoutes` | Low-level HTTP only (optional) |
| `@corbits/memory/sidecar-bundle` | Deployed-agent factory — no client code, no base URL, no token |
| `@corbits/memory/distiller` | Optional process helpers: `runDistillTick`, `createResidentDistiller` |

Expand Down Expand Up @@ -80,7 +80,8 @@ Deployed agent (sidecar-bundle)
┌──────────────────────────────────────────────┐
│ Host Interchange createApp │
│ principal + tenant on context │
│ + createMemory({ app, grantStore, … }) │
│ + createMemory({ grantStore, … }) │
│ + app.route(…, createMemoryRoutes(deps)) │
│ grants: memory:add | memory:search │
│ documentStore: pgvector | host | fake │
│ + mountWorkflowMemory(app, { memory, … }) │
Expand Down
39 changes: 23 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,38 @@ yarn add @corbits/memory
bun add @corbits/memory
```

Write the mount as a function that takes your hub's `app`, `grantStore`, and
`conditionRegistry` — the same trio you already pass to
Build the plane, then mount its routes on your hub's `app` with the
`grantStore` and `conditionRegistry` you already pass to
`createRequireGrant`/`createApp`:

```ts
import { Hono } from "hono";
import type { TenantEnv } from "@intx/hub-api";
import { createRequireGrant, type TenantEnv } from "@intx/hub-api";
import type { ConditionRegistry, GrantStore } from "@intx/authz";
import { createMemory, loadMemoryConfig, type Memory } from "@corbits/memory";
import {
createMemory,
createMemoryRoutes,
loadMemoryConfig,
type Memory,
} from "@corbits/memory";

export function installMemory(
app: Hono<TenantEnv>,
grantStore: GrantStore,
conditionRegistry: ConditionRegistry,
): Memory {
const memoryApp = new Hono<TenantEnv>();
const memory = createMemory({
app: memoryApp,
config: loadMemoryConfig(), // DATABASE_URL + embed env — see below
grantStore,
conditionRegistry,
});
app.route("/", memoryApp);
app.route(
"/api/tenants/:tenantId/memory",
createMemoryRoutes({
memory,
requireGrant: createRequireGrant({ grantStore, conditionRegistry }),
}),
);
return memory;
}
```
Expand Down Expand Up @@ -125,10 +134,9 @@ at `@corbits/memory/sidecar-bundle`.

## How it works

`createMemory` builds the plane. Pass `app` to register
`/api/tenants/:tenantId/memory/*` behind `requireGrant("memory", …)` —
`grantStore` is required for that mount. `loadMemoryConfig` lives on the
barrel and at `@corbits/memory/config`.
`createMemory` builds the plane. `createMemoryRoutes` returns its HTTP routes
as a Hono sub-app, each guarded by `requireGrant("memory", …)`.
`loadMemoryConfig` lives on the barrel and at `@corbits/memory/config`.

Capability grants (`memory:add` / `memory:search` / `memory:forget` /
`memory:purge`) gate the routes. Per-document visibility is Interchange
Expand Down Expand Up @@ -167,9 +175,8 @@ mail-triggered workflow is ticked by addressing it directly. No import of

### Lower-level: in-process calls, no HTTP

`app` is optional. Passing only `config` builds the plane without
registering routes, for a host worker that calls `add`/`search` directly
(the resident distiller does this):
A host worker can skip `createMemoryRoutes` and call `add`/`search` on the
plane directly (the resident distiller does this):

```ts
import { createMemory, loadMemoryConfig } from "@corbits/memory";
Expand Down Expand Up @@ -203,8 +210,8 @@ bun run typecheck # tsc --noEmit
bun run test # bun test ./src
```

Tests use `createFakeDocumentStore`/`createFakeSourceProvider` (exported for
this purpose) so the suite runs without Postgres. `bun run build` compiles
Unit tests use the in-repo `createFakeDocumentStore`/`createFakeSourceProvider`
so the suite runs without Postgres. `bun run build` compiles
`src/` to `dist/`, which is what the package publishes.

## License
Expand Down
2 changes: 1 addition & 1 deletion src/core/merge-local-live.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* MergeLocalLiveV1 — combine local DocumentStore hits with live SourceProvider
* Local/live merge — combine local DocumentStore hits with live SourceProvider
* hits into one ranked list.
*
* Spec (frozen for M3/M4):
Expand Down
Loading
Loading