Files
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

1643 lines
55 KiB
TypeScript

/**
* InMemoryDatabaseAdapter
*
* Pure in-memory, ephemeral implementation of the elizaOS `DatabaseAdapter`
* abstract base. Backed by `MemoryStorage` (a Map-of-Maps) for record CRUD and
* `EphemeralHNSW` for vector search. No persistence, no migrations, no
* transaction atomicity — intended for tests, scenarios, and short-lived
* one-shot runs where speed and zero setup matter more than durability.
*
* Implements the full batch-first interface from `@elizaos/core`'s
* `DatabaseAdapter`; there are no single-item helpers — call sites use the
* batch APIs (`createEntities`, `getMemoriesByIds`, etc.).
*/
import { randomUUID } from "node:crypto";
import {
type AccessContext,
type Agent,
type Component,
type Content,
DatabaseAdapter,
type EntitiesForRoomsResult,
type Entity,
type IDatabaseAdapter,
type JsonValue,
type Log,
type LogBody,
logger,
type Memory,
type MemoryMetadata,
type MemoryTypeAlias,
type MessageSearchHit,
type Metadata,
type PairingAllowlistEntry,
type PairingAllowlistsResult,
type PairingChannel,
type PairingRequest,
type PairingRequestsResult,
type Participant,
type ParticipantsForRoomsResult,
type ParticipantUpdateFields,
type ParticipantUserState,
type PatchOp,
type Relationship,
type Room,
rankMessageSearch,
type Task,
type UUID,
type World,
withinCreatedAtWindow,
} from "@elizaos/core";
import { EphemeralHNSW } from "./hnsw";
import { COLLECTIONS, type IStorage } from "./types";
// ────────────────────────────────────────────────────────────────────────────
// Internal stored shapes
// ────────────────────────────────────────────────────────────────────────────
interface StoredParticipant {
id: string;
entityId: string;
roomId: string;
userState?: ParticipantUserState;
metadata?: Record<string, unknown>;
}
interface StoredMemory {
id?: string;
entityId: string;
agentId?: string;
createdAt?: number;
content: Content;
embedding?: number[];
roomId: string;
worldId?: string;
unique?: boolean;
similarity?: number;
metadata?: MemoryMetadata;
}
interface StoredRelationship {
id: string;
sourceEntityId: string;
targetEntityId: string;
agentId?: string;
tags?: string[];
metadata?: Metadata;
createdAt?: string;
}
interface StoredCacheEntry<T = unknown> {
value: T;
expiresAt?: number;
}
// ────────────────────────────────────────────────────────────────────────────
// Helpers
// ────────────────────────────────────────────────────────────────────────────
function toMemory(stored: StoredMemory): Memory {
return {
id: stored.id as UUID | undefined,
entityId: stored.entityId as UUID,
agentId: stored.agentId as UUID | undefined,
createdAt: stored.createdAt,
content: stored.content,
embedding: stored.embedding,
roomId: stored.roomId as UUID,
worldId: stored.worldId as UUID | undefined,
unique: stored.unique,
similarity: stored.similarity,
metadata: stored.metadata,
};
}
function relationshipFromStored(r: StoredRelationship, fallbackAgentId: UUID): Relationship {
return {
id: r.id as UUID,
sourceEntityId: r.sourceEntityId as UUID,
targetEntityId: r.targetEntityId as UUID,
agentId: (r.agentId as UUID) ?? fallbackAgentId,
tags: r.tags ?? [],
metadata: r.metadata ?? {},
createdAt: r.createdAt,
};
}
/**
* Apply a single JSON patch operation to a deeply-nested target object,
* resolving the dot-separated `path` to a leaf and mutating in place.
*
* This is a best-effort implementation that mirrors what Postgres's JSONB
* patch operators do for the SQL adapter. It supports `set`, `push`,
* `remove`, and `increment`.
*/
function applyPatchOp(target: Record<string, unknown>, op: PatchOp): void {
if (!op.path) return;
const parts = op.path.split(".");
const last = parts.pop();
if (last === undefined) return;
let parent: Record<string, unknown> = target;
for (const segment of parts) {
const next = parent[segment];
if (next === null || typeof next !== "object") {
const created: Record<string, unknown> = {};
parent[segment] = created;
parent = created;
} else {
parent = next as Record<string, unknown>;
}
}
switch (op.op) {
case "set":
parent[last] = op.value;
break;
case "remove":
delete parent[last];
break;
case "push": {
const existing = parent[last];
if (Array.isArray(existing)) {
existing.push(op.value);
} else {
parent[last] = [op.value];
}
break;
}
case "increment": {
const existing = parent[last];
const delta = typeof op.value === "number" ? op.value : 1;
parent[last] = typeof existing === "number" ? existing + delta : delta;
break;
}
}
}
function levenshtein(a: string, b: string): number {
if (a === b) return 0;
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
const prev = new Array(b.length + 1);
const curr = new Array(b.length + 1);
for (let j = 0; j <= b.length; j++) prev[j] = j;
for (let i = 1; i <= a.length; i++) {
curr[0] = i;
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
}
for (let j = 0; j <= b.length; j++) prev[j] = curr[j];
}
return curr[b.length];
}
// ────────────────────────────────────────────────────────────────────────────
// Adapter
// ────────────────────────────────────────────────────────────────────────────
export class InMemoryDatabaseAdapter extends DatabaseAdapter<IStorage> {
private storage: IStorage;
private vectorIndex: EphemeralHNSW;
private embeddingDimension = 384;
private ready = false;
private readonly agentId: UUID;
constructor(storage: IStorage, agentId: UUID) {
super();
this.storage = storage;
this.agentId = agentId;
this.db = storage;
this.vectorIndex = new EphemeralHNSW();
}
// ── Lifecycle ─────────────────────────────────────────────────────────
async initialize(_config?: Record<string, string | number | boolean | null>): Promise<void> {
await this.storage.init();
await this.vectorIndex.init(this.embeddingDimension);
this.ready = true;
logger.info({ src: "plugin:inmemorydb" }, "In-memory database initialized");
}
/** Backward-compat alias used by `plugin-inmemorydb`'s plugin init hook. */
async init(): Promise<void> {
await this.initialize();
}
async runPluginMigrations(
_plugins: Array<{ name: string; schema?: Record<string, JsonValue> }>,
_options?: { verbose?: boolean; force?: boolean; dryRun?: boolean }
): Promise<void> {
logger.debug(
{ src: "plugin:inmemorydb" },
"Plugin migrations not needed for in-memory storage"
);
}
async isReady(): Promise<boolean> {
return this.ready && (await this.storage.isReady());
}
async close(): Promise<void> {
await this.vectorIndex.clear();
await this.storage.close();
this.ready = false;
logger.info({ src: "plugin:inmemorydb" }, "In-memory database closed");
}
async getConnection(): Promise<IStorage> {
return this.storage;
}
// ── Transactions ──────────────────────────────────────────────────────
// No atomicity guarantees: just invoke the callback with `this`. This
// matches what other in-memory adapters do — callers that need real
// transactions should use a real database backend.
async transaction<T>(
callback: (tx: IDatabaseAdapter<IStorage>) => Promise<T>,
_options?: { entityContext?: UUID }
): Promise<T> {
return callback(this as IDatabaseAdapter<IStorage>);
}
// ── Embedding ─────────────────────────────────────────────────────────
async ensureEmbeddingDimension(dimension: number): Promise<void> {
if (this.embeddingDimension !== dimension) {
this.embeddingDimension = dimension;
await this.vectorIndex.init(dimension);
}
}
async clearEmbeddingsOutsideActiveDimension(): Promise<UUID[]> {
const embeddedMemories = await this.storage.getWhere<StoredMemory>(
COLLECTIONS.MEMORIES,
(memory) => Array.isArray(memory.embedding) && memory.embedding.length > 0
);
const reclaimed: UUID[] = [];
await this.vectorIndex.clear();
await this.vectorIndex.init(this.embeddingDimension);
for (const memory of embeddedMemories) {
const id = memory.id as UUID | undefined;
if (!id || !memory.embedding) continue;
if (memory.embedding.length === this.embeddingDimension) {
await this.vectorIndex.add(id, memory.embedding);
continue;
}
const { embedding: _embedding, ...withoutEmbedding } = memory;
await this.storage.set(COLLECTIONS.MEMORIES, id, withoutEmbedding);
reclaimed.push(id);
}
return reclaimed;
}
// ── Entity CRUD ───────────────────────────────────────────────────────
async createEntities(entities: Entity[]): Promise<UUID[]> {
const ids: UUID[] = [];
for (const entity of entities) {
const id = (entity.id ?? randomUUID()) as UUID;
await this.storage.set(COLLECTIONS.ENTITIES, id, { ...entity, id });
ids.push(id);
}
return ids;
}
async upsertEntities(entities: Entity[]): Promise<void> {
for (const entity of entities) {
if (!entity.id) continue;
const existing = await this.storage.get<Entity>(COLLECTIONS.ENTITIES, entity.id);
await this.storage.set(COLLECTIONS.ENTITIES, entity.id, {
...(existing ?? {}),
...entity,
});
}
}
async getEntitiesByIds(entityIds: UUID[]): Promise<Entity[]> {
const entities: Entity[] = [];
for (const id of entityIds) {
const entity = await this.storage.get<Entity>(COLLECTIONS.ENTITIES, id);
if (entity) entities.push(entity);
}
return entities;
}
async updateEntities(entities: Entity[]): Promise<void> {
for (const entity of entities) {
if (!entity.id) continue;
const existing = await this.storage.get<Entity>(COLLECTIONS.ENTITIES, entity.id);
if (!existing) continue;
await this.storage.set(COLLECTIONS.ENTITIES, entity.id, {
...existing,
...entity,
});
}
}
async deleteEntities(entityIds: UUID[]): Promise<void> {
for (const id of entityIds) {
await this.storage.delete(COLLECTIONS.ENTITIES, id);
}
}
async getEntitiesForRooms(
roomIds: UUID[],
includeComponents = false
): Promise<EntitiesForRoomsResult> {
const result: EntitiesForRoomsResult = [];
for (const roomId of roomIds) {
const participants = await this.storage.getWhere<StoredParticipant>(
COLLECTIONS.PARTICIPANTS,
(p) => p.roomId === roomId
);
const entityIds = [...new Set(participants.map((p) => p.entityId))] as UUID[];
const entities = await this.getEntitiesByIds(entityIds);
if (includeComponents) {
for (const entity of entities) {
if (!entity.id) continue;
const components = await this.getComponentsForEntities([entity.id]);
(entity as Entity & { components?: Component[] }).components = components;
}
}
result.push({ roomId, entities });
}
return result;
}
async getEntitiesByNames(params: { names: string[]; agentId: UUID }): Promise<Entity[]> {
if (params.names.length === 0) return [];
const set = new Set(params.names);
return this.storage.getWhere<Entity>(COLLECTIONS.ENTITIES, (e) => {
const names = (e as Entity & { names?: string[] }).names ?? [];
return names.some((name) => set.has(name));
});
}
async searchEntitiesByName(params: {
query: string;
agentId: UUID;
limit?: number;
}): Promise<Entity[]> {
const q = params.query.toLowerCase();
const matches = await this.storage.getWhere<Entity>(COLLECTIONS.ENTITIES, (e) => {
const names = (e as Entity & { names?: string[] }).names ?? [];
return names.some((name) => name.toLowerCase().includes(q));
});
return params.limit ? matches.slice(0, params.limit) : matches;
}
async queryEntities(params: {
componentType?: string;
componentDataFilter?: Record<string, unknown>;
agentId?: UUID;
entityIds?: UUID[];
worldId?: UUID;
limit?: number;
offset?: number;
includeAllComponents?: boolean;
entityContext?: UUID;
}): Promise<Entity[]> {
let entityIds: UUID[];
if (params.entityIds && params.entityIds.length > 0) {
entityIds = params.entityIds;
} else {
const allComponents = await this.storage.getWhere<Component>(COLLECTIONS.COMPONENTS, (c) => {
if (params.componentType && c.type !== params.componentType) return false;
if (params.worldId && c.worldId !== params.worldId) return false;
if (params.componentDataFilter) {
const data = (c as Component & { data?: Record<string, unknown> }).data;
if (!data || typeof data !== "object") return false;
for (const [k, v] of Object.entries(params.componentDataFilter)) {
if (data[k] !== v) return false;
}
}
return true;
});
entityIds = [...new Set(allComponents.map((c) => c.entityId as UUID))];
}
const offset = params.offset ?? 0;
const limit = params.limit;
const sliced =
limit !== undefined ? entityIds.slice(offset, offset + limit) : entityIds.slice(offset);
const entities = await this.getEntitiesByIds(sliced);
if (params.includeAllComponents) {
for (const entity of entities) {
if (!entity.id) continue;
const components = await this.getComponentsForEntities([entity.id]);
(entity as Entity & { components?: Component[] }).components = components;
}
}
return entities;
}
// ── Component CRUD ────────────────────────────────────────────────────
async createComponents(components: Component[]): Promise<UUID[]> {
const ids: UUID[] = [];
for (const component of components) {
const id = component.id as UUID;
await this.storage.set(COLLECTIONS.COMPONENTS, id, { ...component, id });
ids.push(id);
}
return ids;
}
async getComponentsByIds(componentIds: UUID[]): Promise<Component[]> {
const components: Component[] = [];
for (const id of componentIds) {
const c = await this.storage.get<Component>(COLLECTIONS.COMPONENTS, id);
if (c) components.push(c);
}
return components;
}
async updateComponents(components: Component[]): Promise<void> {
for (const component of components) {
if (!component.id) continue;
const existing = await this.storage.get<Component>(COLLECTIONS.COMPONENTS, component.id);
if (!existing) continue;
await this.storage.set(COLLECTIONS.COMPONENTS, component.id, {
...existing,
...component,
});
}
}
async deleteComponents(componentIds: UUID[]): Promise<void> {
for (const id of componentIds) {
await this.storage.delete(COLLECTIONS.COMPONENTS, id);
}
}
async upsertComponents(
components: Component[],
_options?: { entityContext?: UUID }
): Promise<void> {
for (const component of components) {
const naturalKey = await this.storage.getWhere<Component>(
COLLECTIONS.COMPONENTS,
(c) =>
c.entityId === component.entityId &&
c.type === component.type &&
c.worldId === component.worldId &&
c.sourceEntityId === component.sourceEntityId
);
const existing = naturalKey[0];
if (existing) {
await this.storage.set(COLLECTIONS.COMPONENTS, existing.id, {
...existing,
...component,
id: existing.id,
});
} else {
const id = component.id as UUID;
await this.storage.set(COLLECTIONS.COMPONENTS, id, {
...component,
id,
});
}
}
}
async patchComponents(
updates: Array<{ componentId: UUID; ops: PatchOp[] }>,
_options?: { entityContext?: UUID }
): Promise<void> {
for (const update of updates) {
const component = await this.storage.get<Component>(
COLLECTIONS.COMPONENTS,
update.componentId
);
if (!component) continue;
const data = { ...(component.data ?? {}) } as Record<string, unknown>;
for (const op of update.ops) {
applyPatchOp(data, op);
}
component.data = data as Component["data"];
await this.storage.set(COLLECTIONS.COMPONENTS, update.componentId, component);
}
}
async getComponentsByNaturalKeys(
keys: Array<{
entityId: UUID;
type: string;
worldId?: UUID;
sourceEntityId?: UUID;
}>
): Promise<(Component | null)[]> {
const result: (Component | null)[] = [];
for (const key of keys) {
const matches = await this.storage.getWhere<Component>(
COLLECTIONS.COMPONENTS,
(c) =>
c.entityId === key.entityId &&
c.type === key.type &&
c.worldId === (key.worldId ?? null) &&
c.sourceEntityId === (key.sourceEntityId ?? null)
);
result.push(matches[0] ?? null);
}
return result;
}
async getComponentsForEntities(
entityIds: UUID[],
worldId?: UUID,
sourceEntityId?: UUID
): Promise<Component[]> {
if (entityIds.length === 0) return [];
const idSet = new Set(entityIds);
return this.storage.getWhere<Component>(COLLECTIONS.COMPONENTS, (c) => {
if (!idSet.has(c.entityId as UUID)) return false;
if (worldId !== undefined && c.worldId !== worldId) return false;
if (sourceEntityId !== undefined && c.sourceEntityId !== sourceEntityId) return false;
return true;
});
}
// ── Memory CRUD ───────────────────────────────────────────────────────
async getMemories(params: {
entityId?: UUID;
agentId?: UUID;
limit?: number;
count?: number;
offset?: number;
unique?: boolean;
tableName: string;
start?: number;
end?: number;
roomId?: UUID;
worldId?: UUID;
metadata?: Record<string, unknown>;
textContains?: string;
orderBy?: "createdAt";
orderDirection?: "asc" | "desc";
includeEmbedding?: boolean;
accessContext?: AccessContext;
}): Promise<Memory[]> {
const textContains = params.textContains?.trim().toLowerCase();
let memories = await this.storage.getWhere<StoredMemory>(COLLECTIONS.MEMORIES, (m) => {
if (params.entityId && m.entityId !== params.entityId) return false;
if (params.agentId && m.agentId !== params.agentId) return false;
if (params.roomId && m.roomId !== params.roomId) return false;
if (params.worldId && m.worldId !== params.worldId) return false;
if (params.tableName && m.metadata?.type !== params.tableName) return false;
if (params.start && m.createdAt && m.createdAt < params.start) return false;
if (params.end && m.createdAt && m.createdAt > params.end) return false;
if (params.unique && !m.unique) return false;
if (params.metadata) {
const md = (m.metadata ?? {}) as Record<string, unknown>;
for (const [k, v] of Object.entries(params.metadata)) {
if (md[k] !== v) return false;
}
}
if (textContains) {
const text = (m.content as { text?: unknown } | undefined)?.text;
if (typeof text !== "string" || !text.toLowerCase().includes(textContains)) {
return false;
}
}
return true;
});
const direction = params.orderDirection ?? "desc";
memories.sort((a, b) => {
const ta = typeof a.createdAt === "number" ? a.createdAt : 0;
const tb = typeof b.createdAt === "number" ? b.createdAt : 0;
if (ta !== tb) return direction === "asc" ? ta - tb : tb - ta;
const aId = typeof a.id === "string" ? a.id : "";
const bId = typeof b.id === "string" ? b.id : "";
return direction === "asc" ? aId.localeCompare(bId) : bId.localeCompare(aId);
});
const offset = typeof params.offset === "number" ? params.offset : 0;
const limit = params.limit ?? params.count;
if (offset > 0) memories = memories.slice(offset);
if (limit !== undefined) memories = memories.slice(0, limit);
return memories.map(toMemory);
}
async getMemoriesByRoomIds(params: {
roomIds: UUID[];
tableName: string;
limit?: number;
offset?: number;
textContains?: string;
includeEmbedding?: boolean;
accessContext?: AccessContext;
}): Promise<Memory[]> {
if (params.roomIds.length === 0) return [];
const roomSet = new Set(params.roomIds);
const textContains = params.textContains?.trim().toLowerCase();
const memories = await this.storage.getWhere<StoredMemory>(COLLECTIONS.MEMORIES, (m) => {
if (!roomSet.has(m.roomId as UUID)) return false;
if (params.tableName && m.metadata?.type !== params.tableName) return false;
// Same case-insensitive `includes` semantics the SQL adapter pushes
// down as ILIKE.
if (
textContains &&
!String(m.content.text ?? "")
.toLowerCase()
.includes(textContains)
) {
return false;
}
return true;
});
memories.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
const offset = typeof params.offset === "number" ? params.offset : 0;
let sliced = offset > 0 ? memories.slice(offset) : memories;
if (params.limit !== undefined) sliced = sliced.slice(0, params.limit);
return sliced.map(toMemory);
}
async searchMessages(params: {
roomIds: UUID[];
query: string;
tableName?: string;
limit?: number;
offset?: number;
since?: number;
until?: number;
accessContext?: AccessContext;
}): Promise<MessageSearchHit[]> {
if (params.roomIds.length === 0) return [];
const roomSet = new Set(params.roomIds);
const tableName = params.tableName ?? "messages";
const stored = await this.storage.getWhere<StoredMemory>(
COLLECTIONS.MEMORIES,
(m) => roomSet.has(m.roomId as UUID) && m.metadata?.type === tableName
);
// The window is applied before ranking + LIMIT/OFFSET, mirroring the SQL
// adapters' created_at range conditions.
const candidates = stored
.map(toMemory)
.filter((memory) =>
withinCreatedAtWindow(
typeof memory.createdAt === "number" ? memory.createdAt : undefined,
params.since,
params.until
)
);
const ranked = rankMessageSearch(candidates, params.query);
const offset = typeof params.offset === "number" ? params.offset : 0;
const limit = params.limit ?? 20;
return ranked.slice(offset, offset + limit).map(({ item, ftsRank, trigramSimilarity }) => ({
memory: item,
ftsRank,
trigramSimilarity,
}));
}
async getMemoriesByIds(memoryIds: UUID[], tableName?: string): Promise<Memory[]> {
const memories: Memory[] = [];
for (const id of memoryIds) {
const m = await this.storage.get<StoredMemory>(COLLECTIONS.MEMORIES, id);
if (!m) continue;
if (tableName && m.metadata?.type !== tableName) continue;
memories.push(toMemory(m));
}
return memories;
}
async getCachedEmbeddings(params: {
query_table_name: string;
query_threshold: number;
query_input: string;
query_field_name: string;
query_field_sub_name: string;
query_match_count: number;
}): Promise<{ embedding: number[]; levenshtein_score: number }[]> {
const memories = await this.storage.getWhere<StoredMemory>(
COLLECTIONS.MEMORIES,
(m) => m.metadata?.type === params.query_table_name && !!m.embedding
);
const results: { embedding: number[]; levenshtein_score: number }[] = [];
for (const memory of memories) {
if (!memory.embedding) continue;
const record = memory as StoredMemory & Record<string, unknown>;
const fieldValue = record[params.query_field_name];
const text = String(fieldValue ?? "");
const score = levenshtein(params.query_input, text);
if (score <= params.query_threshold) {
results.push({ embedding: memory.embedding, levenshtein_score: score });
}
}
results.sort((a, b) => a.levenshtein_score - b.levenshtein_score);
return results.slice(0, params.query_match_count);
}
async searchMemories(params: {
tableName: string;
embedding: number[];
match_threshold?: number;
count?: number;
limit?: number;
unique?: boolean;
query?: string;
roomId?: UUID;
worldId?: UUID;
entityId?: UUID;
accessContext?: AccessContext;
}): Promise<Memory[]> {
const threshold = params.match_threshold ?? 0.5;
const limit = params.count ?? params.limit ?? 10;
const results = await this.vectorIndex.search(params.embedding, limit * 2, threshold);
const memories: Memory[] = [];
for (const result of results) {
const memory = await this.storage.get<StoredMemory>(COLLECTIONS.MEMORIES, result.id);
if (!memory) continue;
if (params.tableName && memory.metadata?.type !== params.tableName) continue;
if (params.roomId && memory.roomId !== params.roomId) continue;
if (params.worldId && memory.worldId !== params.worldId) continue;
if (params.entityId && memory.entityId !== params.entityId) continue;
if (params.unique && !memory.unique) continue;
memories.push({ ...toMemory(memory), similarity: result.similarity });
}
return memories.slice(0, limit);
}
async createMemories(
memories: Array<{ memory: Memory; tableName: string; unique?: boolean }>
): Promise<UUID[]> {
const ids: UUID[] = [];
for (const { memory, tableName, unique = false } of memories) {
const id = (memory.id ?? randomUUID()) as UUID;
const stored: StoredMemory = {
...memory,
id,
agentId: memory.agentId ?? this.agentId,
unique: unique || memory.unique,
createdAt: memory.createdAt ?? Date.now(),
metadata: {
...(memory.metadata ?? {}),
type: tableName as MemoryTypeAlias,
} as MemoryMetadata,
};
await this.storage.set(COLLECTIONS.MEMORIES, id, stored);
if (memory.embedding && memory.embedding.length > 0) {
await this.vectorIndex.add(id, memory.embedding);
}
ids.push(id);
}
return ids;
}
async updateMemories(
memories: Array<Partial<Memory> & { id: UUID; metadata?: MemoryMetadata }>
): Promise<void> {
for (const memory of memories) {
const existing = await this.storage.get<StoredMemory>(COLLECTIONS.MEMORIES, memory.id);
if (!existing) continue;
const updated: StoredMemory = {
...existing,
...memory,
metadata: {
...(existing.metadata ?? {}),
...(memory.metadata ?? {}),
} as MemoryMetadata,
};
await this.storage.set(COLLECTIONS.MEMORIES, memory.id, updated);
if (memory.embedding && memory.embedding.length > 0) {
await this.vectorIndex.add(memory.id, memory.embedding);
}
}
}
async upsertMemories(
memories: Array<{ memory: Memory; tableName: string }>,
_options?: { entityContext?: UUID }
): Promise<void> {
for (const { memory, tableName } of memories) {
if (!memory.id) {
await this.createMemories([{ memory, tableName }]);
continue;
}
const existing = await this.storage.get<StoredMemory>(COLLECTIONS.MEMORIES, memory.id);
const stored: StoredMemory = {
...(existing ?? {}),
...memory,
agentId: memory.agentId ?? existing?.agentId ?? this.agentId,
createdAt: memory.createdAt ?? existing?.createdAt ?? Date.now(),
metadata: {
...(existing?.metadata ?? {}),
...(memory.metadata ?? {}),
type: tableName as MemoryTypeAlias,
} as MemoryMetadata,
};
await this.storage.set(COLLECTIONS.MEMORIES, memory.id, stored);
if (memory.embedding && memory.embedding.length > 0) {
await this.vectorIndex.add(memory.id, memory.embedding);
}
}
}
async deleteMemories(memoryIds: UUID[]): Promise<void> {
for (const id of memoryIds) {
await this.storage.delete(COLLECTIONS.MEMORIES, id);
await this.vectorIndex.remove(id);
}
}
async deleteAllMemories(roomIds: UUID[], tableName: string): Promise<void> {
if (roomIds.length === 0) return;
const roomSet = new Set(roomIds);
const memories = await this.storage.getWhere<StoredMemory>(
COLLECTIONS.MEMORIES,
(m) => roomSet.has(m.roomId as UUID) && (tableName ? m.metadata?.type === tableName : true)
);
const ids = memories.map((m) => m.id).filter((id): id is string => id !== undefined) as UUID[];
await this.deleteMemories(ids);
}
async countMemories(params: {
roomIds?: UUID[];
unique?: boolean;
tableName?: string;
entityId?: UUID;
agentId?: UUID;
metadata?: Record<string, unknown>;
}): Promise<number> {
const roomSet = params.roomIds ? new Set(params.roomIds) : null;
return this.storage.count<StoredMemory>(COLLECTIONS.MEMORIES, (m) => {
if (roomSet && !roomSet.has(m.roomId as UUID)) return false;
if (params.unique && !m.unique) return false;
if (params.tableName && m.metadata?.type !== params.tableName) return false;
if (params.entityId && m.entityId !== params.entityId) return false;
if (params.agentId && m.agentId !== params.agentId) return false;
if (params.metadata) {
const md = (m.metadata ?? {}) as Record<string, unknown>;
for (const [k, v] of Object.entries(params.metadata)) {
if (md[k] !== v) return false;
}
}
return true;
});
}
async getMemoriesByWorldId(params: {
worldIds?: UUID[];
limit?: number;
tableName?: string;
}): Promise<Memory[]> {
const worldSet = params.worldIds ? new Set(params.worldIds) : null;
const memories = await this.storage.getWhere<StoredMemory>(
COLLECTIONS.MEMORIES,
(m) =>
(!worldSet || (m.worldId ? worldSet.has(m.worldId as UUID) : false)) &&
(params.tableName ? m.metadata?.type === params.tableName : true)
);
memories.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
const sliced = params.limit ? memories.slice(0, params.limit) : memories;
return sliced.map(toMemory);
}
// ── Log CRUD ──────────────────────────────────────────────────────────
async getLogs(params: {
entityId?: UUID;
roomId?: UUID;
type?: string;
limit?: number;
offset?: number;
}): Promise<Log[]> {
let logs = await this.storage.getWhere<Log>(COLLECTIONS.LOGS, (l) => {
if (params.entityId && l.entityId !== params.entityId) return false;
if (params.roomId && l.roomId !== params.roomId) return false;
if (params.type && l.type !== params.type) return false;
return true;
});
logs.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
const offset = params.offset ?? 0;
if (offset > 0) logs = logs.slice(offset);
if (params.limit !== undefined) logs = logs.slice(0, params.limit);
return logs;
}
async createLogs(
params: Array<{
body: LogBody;
entityId: UUID;
roomId: UUID;
type: string;
}>
): Promise<void> {
for (const entry of params) {
const id = randomUUID() as UUID;
const log: Log = {
id,
entityId: entry.entityId,
roomId: entry.roomId,
body: entry.body,
type: entry.type,
createdAt: new Date(),
};
await this.storage.set(COLLECTIONS.LOGS, id, log);
}
}
async getLogsByIds(logIds: UUID[]): Promise<Log[]> {
const logs: Log[] = [];
for (const id of logIds) {
const log = await this.storage.get<Log>(COLLECTIONS.LOGS, id);
if (log) logs.push(log);
}
return logs;
}
async updateLogs(logs: Array<{ id: UUID; updates: Partial<Log> }>): Promise<void> {
for (const { id, updates } of logs) {
const existing = await this.storage.get<Log>(COLLECTIONS.LOGS, id);
if (!existing) continue;
await this.storage.set(COLLECTIONS.LOGS, id, { ...existing, ...updates });
}
}
async deleteLogs(logIds: UUID[]): Promise<void> {
for (const id of logIds) {
await this.storage.delete(COLLECTIONS.LOGS, id);
}
}
// ── World CRUD ────────────────────────────────────────────────────────
async getAllWorlds(): Promise<World[]> {
return this.storage.getAll<World>(COLLECTIONS.WORLDS);
}
async getWorldsByIds(worldIds: UUID[]): Promise<World[]> {
const worlds: World[] = [];
for (const id of worldIds) {
const w = await this.storage.get<World>(COLLECTIONS.WORLDS, id);
if (w) worlds.push(w);
}
return worlds;
}
async createWorlds(worlds: World[]): Promise<UUID[]> {
const ids: UUID[] = [];
for (const world of worlds) {
const id = world.id as UUID;
await this.storage.set(COLLECTIONS.WORLDS, id, { ...world, id });
ids.push(id);
}
return ids;
}
async deleteWorlds(worldIds: UUID[]): Promise<void> {
for (const id of worldIds) {
await this.storage.delete(COLLECTIONS.WORLDS, id);
}
}
async updateWorlds(worlds: World[]): Promise<void> {
for (const world of worlds) {
if (!world.id) continue;
const existing = await this.storage.get<World>(COLLECTIONS.WORLDS, world.id);
if (!existing) continue;
await this.storage.set(COLLECTIONS.WORLDS, world.id, {
...existing,
...world,
});
}
}
async upsertWorlds(worlds: World[]): Promise<void> {
for (const world of worlds) {
const id = world.id as UUID;
const existing = await this.storage.get<World>(COLLECTIONS.WORLDS, id);
await this.storage.set(COLLECTIONS.WORLDS, id, {
...(existing ?? {}),
...world,
id,
});
}
}
// ── Room CRUD ─────────────────────────────────────────────────────────
async getRoomsByIds(roomIds: UUID[]): Promise<Room[]> {
const rooms: Room[] = [];
for (const id of roomIds) {
const room = await this.storage.get<Room>(COLLECTIONS.ROOMS, id);
if (room) rooms.push(room);
}
return rooms;
}
async deleteRoomsByWorldIds(worldIds: UUID[]): Promise<void> {
if (worldIds.length === 0) return;
const worldSet = new Set(worldIds);
const rooms = await this.storage.getWhere<Room>(COLLECTIONS.ROOMS, (r) =>
r.worldId ? worldSet.has(r.worldId as UUID) : false
);
const roomIds = rooms.map((r) => r.id).filter((id): id is UUID => id !== undefined);
await this.deleteRooms(roomIds);
}
async getRoomsForParticipants(entityIds: UUID[]): Promise<UUID[]> {
if (entityIds.length === 0) return [];
const entitySet = new Set(entityIds);
const participants = await this.storage.getWhere<StoredParticipant>(
COLLECTIONS.PARTICIPANTS,
(p) => entitySet.has(p.entityId as UUID)
);
return [...new Set(participants.map((p) => p.roomId as UUID))];
}
async getRoomsByWorlds(worldIds: UUID[], limit?: number, offset?: number): Promise<Room[]> {
if (worldIds.length === 0) return [];
const worldSet = new Set(worldIds);
let rooms = await this.storage.getWhere<Room>(COLLECTIONS.ROOMS, (r) =>
r.worldId ? worldSet.has(r.worldId as UUID) : false
);
const off = offset ?? 0;
if (off > 0) rooms = rooms.slice(off);
if (limit !== undefined) rooms = rooms.slice(0, limit);
return rooms;
}
async createRooms(rooms: Room[]): Promise<UUID[]> {
const ids: UUID[] = [];
for (const room of rooms) {
const id = room.id as UUID;
await this.storage.set(COLLECTIONS.ROOMS, id, { ...room, id });
ids.push(id);
}
return ids;
}
async upsertRooms(rooms: Room[]): Promise<void> {
for (const room of rooms) {
const id = room.id as UUID;
const existing = await this.storage.get<Room>(COLLECTIONS.ROOMS, id);
await this.storage.set(COLLECTIONS.ROOMS, id, {
...(existing ?? {}),
...room,
id,
});
}
}
async updateRooms(rooms: Room[]): Promise<void> {
for (const room of rooms) {
if (!room.id) continue;
const existing = await this.storage.get<Room>(COLLECTIONS.ROOMS, room.id);
if (!existing) continue;
await this.storage.set(COLLECTIONS.ROOMS, room.id, {
...existing,
...room,
});
}
}
async deleteRooms(roomIds: UUID[]): Promise<void> {
if (roomIds.length === 0) return;
const set = new Set(roomIds);
for (const id of roomIds) {
await this.storage.delete(COLLECTIONS.ROOMS, id);
}
// Cascade: drop participants and memories belonging to these rooms.
await this.storage.deleteWhere<StoredParticipant>(COLLECTIONS.PARTICIPANTS, (p) =>
set.has(p.roomId as UUID)
);
await this.storage.deleteWhere<StoredMemory>(COLLECTIONS.MEMORIES, (m) =>
set.has(m.roomId as UUID)
);
}
// ── Participant CRUD ──────────────────────────────────────────────────
async createRoomParticipants(entityIds: UUID[], roomId: UUID): Promise<UUID[]> {
const ids: UUID[] = [];
for (const entityId of entityIds) {
const existing = await this.storage.getWhere<StoredParticipant>(
COLLECTIONS.PARTICIPANTS,
(p) => p.entityId === entityId && p.roomId === roomId
);
const existingParticipant = existing[0];
if (existingParticipant) {
ids.push(existingParticipant.id as UUID);
continue;
}
const id = randomUUID() as UUID;
const participant: StoredParticipant = { id, entityId, roomId };
await this.storage.set(COLLECTIONS.PARTICIPANTS, id, participant);
ids.push(id);
}
return ids;
}
async deleteParticipants(
participants: Array<{ entityId: UUID; roomId: UUID }>
): Promise<boolean> {
let removed = false;
for (const { entityId, roomId } of participants) {
const matches = await this.storage.getWhere<StoredParticipant>(
COLLECTIONS.PARTICIPANTS,
(p) => p.entityId === entityId && p.roomId === roomId
);
for (const p of matches) {
if (p.id) {
await this.storage.delete(COLLECTIONS.PARTICIPANTS, p.id);
removed = true;
}
}
}
return removed;
}
async updateParticipants(
participants: Array<{
entityId: UUID;
roomId: UUID;
updates: ParticipantUpdateFields;
}>
): Promise<void> {
for (const { entityId, roomId, updates } of participants) {
const matches = await this.storage.getWhere<StoredParticipant>(
COLLECTIONS.PARTICIPANTS,
(p) => p.entityId === entityId && p.roomId === roomId
);
for (const p of matches) {
if (!p.id) continue;
const next: StoredParticipant = {
...p,
userState: updates.roomState ?? p.userState,
metadata: { ...(p.metadata ?? {}), ...(updates.metadata ?? {}) },
};
await this.storage.set(COLLECTIONS.PARTICIPANTS, p.id, next);
}
}
}
async getParticipantsForEntities(entityIds: UUID[]): Promise<Participant[]> {
if (entityIds.length === 0) return [];
const set = new Set(entityIds);
const stored = await this.storage.getWhere<StoredParticipant>(COLLECTIONS.PARTICIPANTS, (p) =>
set.has(p.entityId as UUID)
);
const participants: Participant[] = [];
for (const p of stored) {
const entity = await this.storage.get<Entity>(COLLECTIONS.ENTITIES, p.entityId);
if (entity) participants.push({ id: p.id as UUID, entity });
}
return participants;
}
async getParticipantsForRooms(roomIds: UUID[]): Promise<ParticipantsForRoomsResult> {
const result: ParticipantsForRoomsResult = [];
for (const roomId of roomIds) {
const stored = await this.storage.getWhere<StoredParticipant>(
COLLECTIONS.PARTICIPANTS,
(p) => p.roomId === roomId
);
result.push({
roomId,
entityIds: [...new Set(stored.map((p) => p.entityId as UUID))],
});
}
return result;
}
async areRoomParticipants(pairs: Array<{ roomId: UUID; entityId: UUID }>): Promise<boolean[]> {
const result: boolean[] = [];
for (const { roomId, entityId } of pairs) {
const matches = await this.storage.getWhere<StoredParticipant>(
COLLECTIONS.PARTICIPANTS,
(p) => p.roomId === roomId && p.entityId === entityId
);
result.push(matches.length > 0);
}
return result;
}
async getParticipantUserStates(
pairs: Array<{ roomId: UUID; entityId: UUID }>
): Promise<ParticipantUserState[]> {
const result: ParticipantUserState[] = [];
for (const { roomId, entityId } of pairs) {
const matches = await this.storage.getWhere<StoredParticipant>(
COLLECTIONS.PARTICIPANTS,
(p) => p.roomId === roomId && p.entityId === entityId
);
const state = matches[0]?.userState ?? null;
result.push(state);
}
return result;
}
async updateParticipantUserStates(
updates: Array<{
roomId: UUID;
entityId: UUID;
state: ParticipantUserState;
}>
): Promise<void> {
for (const { roomId, entityId, state } of updates) {
const matches = await this.storage.getWhere<StoredParticipant>(
COLLECTIONS.PARTICIPANTS,
(p) => p.roomId === roomId && p.entityId === entityId
);
for (const p of matches) {
if (!p.id) continue;
await this.storage.set(COLLECTIONS.PARTICIPANTS, p.id, {
...p,
userState: state,
});
}
}
}
// ── Relationship CRUD ─────────────────────────────────────────────────
async getRelationshipsByPairs(
pairs: Array<{ sourceEntityId: UUID; targetEntityId: UUID }>
): Promise<(Relationship | null)[]> {
const result: (Relationship | null)[] = [];
for (const pair of pairs) {
const matches = await this.storage.getWhere<StoredRelationship>(
COLLECTIONS.RELATIONSHIPS,
(r) => r.sourceEntityId === pair.sourceEntityId && r.targetEntityId === pair.targetEntityId
);
const first = matches[0];
result.push(first ? relationshipFromStored(first, this.agentId) : null);
}
return result;
}
async getRelationships(params: {
entityIds?: UUID[];
tags?: string[];
limit?: number;
offset?: number;
}): Promise<Relationship[]> {
const entitySet = params.entityIds ? new Set(params.entityIds) : null;
let stored = await this.storage.getWhere<StoredRelationship>(COLLECTIONS.RELATIONSHIPS, (r) => {
if (entitySet) {
if (!entitySet.has(r.sourceEntityId as UUID) && !entitySet.has(r.targetEntityId as UUID)) {
return false;
}
}
if (params.tags && params.tags.length > 0) {
const tags = r.tags ?? [];
if (!params.tags.some((t) => tags.includes(t))) return false;
}
return true;
});
const offset = params.offset ?? 0;
if (offset > 0) stored = stored.slice(offset);
if (params.limit !== undefined) stored = stored.slice(0, params.limit);
return stored.map((r) => relationshipFromStored(r, this.agentId));
}
async createRelationships(
relationships: Array<{
sourceEntityId: UUID;
targetEntityId: UUID;
tags?: string[];
metadata?: Metadata;
}>
): Promise<UUID[]> {
const ids: UUID[] = [];
for (const rel of relationships) {
const id = randomUUID() as UUID;
const stored: StoredRelationship = {
id,
sourceEntityId: rel.sourceEntityId,
targetEntityId: rel.targetEntityId,
agentId: this.agentId,
tags: rel.tags ?? [],
metadata: rel.metadata ?? {},
createdAt: new Date().toISOString(),
};
await this.storage.set(COLLECTIONS.RELATIONSHIPS, id, stored);
ids.push(id);
}
return ids;
}
async getRelationshipsByIds(relationshipIds: UUID[]): Promise<Relationship[]> {
const relationships: Relationship[] = [];
for (const id of relationshipIds) {
const r = await this.storage.get<StoredRelationship>(COLLECTIONS.RELATIONSHIPS, id);
if (r) relationships.push(relationshipFromStored(r, this.agentId));
}
return relationships;
}
async updateRelationships(relationships: Relationship[]): Promise<void> {
for (const rel of relationships) {
if (!rel.id) continue;
const existing = await this.storage.get<StoredRelationship>(
COLLECTIONS.RELATIONSHIPS,
rel.id
);
if (!existing) continue;
const next: StoredRelationship = {
...existing,
sourceEntityId: rel.sourceEntityId,
targetEntityId: rel.targetEntityId,
agentId: rel.agentId,
tags: rel.tags,
metadata: { ...(existing.metadata ?? {}), ...(rel.metadata ?? {}) },
};
await this.storage.set(COLLECTIONS.RELATIONSHIPS, rel.id, next);
}
}
async deleteRelationships(relationshipIds: UUID[]): Promise<void> {
for (const id of relationshipIds) {
await this.storage.delete(COLLECTIONS.RELATIONSHIPS, id);
}
}
// ── Agent CRUD ────────────────────────────────────────────────────────
async getAgents(): Promise<Partial<Agent>[]> {
return this.storage.getAll<Agent>(COLLECTIONS.AGENTS);
}
async getAgentsByIds(agentIds: UUID[]): Promise<Agent[]> {
const agents: Agent[] = [];
for (const id of agentIds) {
const agent = await this.storage.get<Agent>(COLLECTIONS.AGENTS, id);
if (agent) agents.push(agent);
}
return agents;
}
async createAgents(agents: Partial<Agent>[]): Promise<UUID[]> {
const ids: UUID[] = [];
for (const agent of agents) {
const id = (agent.id ?? randomUUID()) as UUID;
await this.storage.set(COLLECTIONS.AGENTS, id, { ...agent, id });
ids.push(id);
}
return ids;
}
async updateAgents(updates: Array<{ agentId: UUID; agent: Partial<Agent> }>): Promise<boolean> {
let updated = false;
for (const { agentId, agent } of updates) {
const existing = await this.storage.get<Agent>(COLLECTIONS.AGENTS, agentId);
if (!existing) continue;
await this.storage.set(COLLECTIONS.AGENTS, agentId, {
...existing,
...agent,
});
updated = true;
}
return updated;
}
async upsertAgents(agents: Partial<Agent>[]): Promise<void> {
for (const agent of agents) {
const id = (agent.id ?? randomUUID()) as UUID;
const existing = await this.storage.get<Agent>(COLLECTIONS.AGENTS, id);
await this.storage.set(COLLECTIONS.AGENTS, id, {
...(existing ?? {}),
...agent,
id,
});
}
}
async deleteAgents(agentIds: UUID[]): Promise<boolean> {
let removed = false;
for (const id of agentIds) {
const ok = await this.storage.delete(COLLECTIONS.AGENTS, id);
if (ok) removed = true;
}
return removed;
}
async countAgents(): Promise<number> {
return this.storage.count(COLLECTIONS.AGENTS);
}
async cleanupAgents(): Promise<void> {
// Nothing to clean up for ephemeral storage.
}
// ── Cache CRUD ────────────────────────────────────────────────────────
async getCaches<T>(keys: string[]): Promise<Map<string, T>> {
const out = new Map<string, T>();
for (const key of keys) {
const entry = await this.storage.get<StoredCacheEntry<T>>(COLLECTIONS.CACHE, key);
if (!entry) continue;
if (entry.expiresAt && Date.now() > entry.expiresAt) {
await this.storage.delete(COLLECTIONS.CACHE, key);
continue;
}
out.set(key, entry.value);
}
return out;
}
async setCaches<T>(entries: Array<{ key: string; value: T }>): Promise<boolean> {
for (const { key, value } of entries) {
await this.storage.set(COLLECTIONS.CACHE, key, { value });
}
return true;
}
async deleteCaches(keys: string[]): Promise<boolean> {
let removed = false;
for (const key of keys) {
const ok = await this.storage.delete(COLLECTIONS.CACHE, key);
if (ok) removed = true;
}
return removed;
}
// ── Task CRUD ─────────────────────────────────────────────────────────
async getTasks(params: {
roomId?: UUID;
tags?: string[];
entityId?: UUID;
agentIds: UUID[];
limit?: number;
offset?: number;
}): Promise<Task[]> {
const agentSet = new Set(params.agentIds);
let tasks = await this.storage.getWhere<Task>(COLLECTIONS.TASKS, (t) => {
const taskAgentId = (t as Task & { agentId?: UUID }).agentId;
if (agentSet.size > 0 && taskAgentId !== undefined && !agentSet.has(taskAgentId)) {
return false;
}
if (params.roomId && t.roomId !== params.roomId) return false;
if (params.entityId && t.entityId !== params.entityId) return false;
if (params.tags && params.tags.length > 0) {
const tags = t.tags ?? [];
if (!params.tags.some((tag) => tags.includes(tag))) return false;
}
return true;
});
const offset = params.offset ?? 0;
if (offset > 0) tasks = tasks.slice(offset);
if (params.limit !== undefined) tasks = tasks.slice(0, params.limit);
return tasks;
}
async getTasksByName(name: string): Promise<Task[]> {
return this.storage.getWhere<Task>(COLLECTIONS.TASKS, (t) => t.name === name);
}
async createTasks(tasks: Task[]): Promise<UUID[]> {
const ids: UUID[] = [];
for (const task of tasks) {
const id = (task.id ?? randomUUID()) as UUID;
await this.storage.set(COLLECTIONS.TASKS, id, { ...task, id });
ids.push(id);
}
return ids;
}
async getTasksByIds(taskIds: UUID[]): Promise<Task[]> {
const tasks: Task[] = [];
for (const id of taskIds) {
const task = await this.storage.get<Task>(COLLECTIONS.TASKS, id);
if (task) tasks.push(task);
}
return tasks;
}
async updateTasks(updates: Array<{ id: UUID; task: Partial<Task> }>): Promise<void> {
for (const { id, task } of updates) {
const existing = await this.storage.get<Task>(COLLECTIONS.TASKS, id);
if (!existing) continue;
await this.storage.set(COLLECTIONS.TASKS, id, { ...existing, ...task });
}
}
async deleteTasks(taskIds: UUID[]): Promise<void> {
for (const id of taskIds) {
await this.storage.delete(COLLECTIONS.TASKS, id);
}
}
// ── Pairing CRUD ──────────────────────────────────────────────────────
async getPairingRequests(
queries: Array<{ channel: PairingChannel; agentId: UUID }>
): Promise<PairingRequestsResult> {
const result: PairingRequestsResult = [];
for (const { channel, agentId } of queries) {
const requests = await this.storage.getWhere<PairingRequest>(
COLLECTIONS.PAIRING_REQUESTS,
(r) => r.channel === channel && r.agentId === agentId
);
result.push({ channel, agentId, requests });
}
return result;
}
async getPairingAllowlists(
queries: Array<{ channel: PairingChannel; agentId: UUID }>
): Promise<PairingAllowlistsResult> {
const result: PairingAllowlistsResult = [];
for (const { channel, agentId } of queries) {
const entries = await this.storage.getWhere<PairingAllowlistEntry>(
COLLECTIONS.PAIRING_ALLOWLIST,
(e) => e.channel === channel && e.agentId === agentId
);
result.push({ channel, agentId, entries });
}
return result;
}
async createPairingRequests(requests: PairingRequest[]): Promise<UUID[]> {
const ids: UUID[] = [];
for (const request of requests) {
const id = request.id as UUID;
await this.storage.set(COLLECTIONS.PAIRING_REQUESTS, id, {
...request,
id,
});
ids.push(id);
}
return ids;
}
async updatePairingRequests(requests: PairingRequest[]): Promise<void> {
for (const request of requests) {
if (!request.id) continue;
const existing = await this.storage.get<PairingRequest>(
COLLECTIONS.PAIRING_REQUESTS,
request.id
);
if (!existing) continue;
await this.storage.set(COLLECTIONS.PAIRING_REQUESTS, request.id, {
...existing,
...request,
});
}
}
async deletePairingRequests(ids: UUID[]): Promise<void> {
for (const id of ids) {
await this.storage.delete(COLLECTIONS.PAIRING_REQUESTS, id);
}
}
async createPairingAllowlistEntries(entries: PairingAllowlistEntry[]): Promise<UUID[]> {
const ids: UUID[] = [];
for (const entry of entries) {
const id = entry.id as UUID;
await this.storage.set(COLLECTIONS.PAIRING_ALLOWLIST, id, {
...entry,
id,
});
ids.push(id);
}
return ids;
}
async updatePairingAllowlistEntries(entries: PairingAllowlistEntry[]): Promise<void> {
for (const entry of entries) {
if (!entry.id) continue;
const existing = await this.storage.get<PairingAllowlistEntry>(
COLLECTIONS.PAIRING_ALLOWLIST,
entry.id
);
if (!existing) continue;
await this.storage.set(COLLECTIONS.PAIRING_ALLOWLIST, entry.id, {
...existing,
...entry,
});
}
}
async deletePairingAllowlistEntries(ids: UUID[]): Promise<void> {
for (const id of ids) {
await this.storage.delete(COLLECTIONS.PAIRING_ALLOWLIST, id);
}
}
}