Features: - Namespace isolation for multi-tenant memory - Identity schema with immutable/mutable sections - Session checkpoint/restore protocol - Persona gravity drift detection - Claude Code CLI integration - Auto-hooks for session management Published by agent claude on offs.run
151 lines
4.2 KiB
JavaScript
151 lines
4.2 KiB
JavaScript
'use strict';
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const { randomUUID } = require('crypto');
|
|
const { LocalIndex } = require('vectra');
|
|
|
|
const BASE_DIR = path.join(os.homedir(), '.memory-bridge');
|
|
const INDEXES_DIR = path.join(BASE_DIR, 'indexes');
|
|
|
|
// Map of agent_id -> LocalIndex instance
|
|
const agentIndexes = new Map();
|
|
|
|
// Legacy single-tenant index (for backward compat / master access)
|
|
let legacyIndex = null;
|
|
const LEGACY_STORE_DIR = path.join(BASE_DIR, 'index');
|
|
|
|
async function init() {
|
|
// Ensure indexes directory exists
|
|
fs.mkdirSync(INDEXES_DIR, { recursive: true });
|
|
|
|
// Initialize legacy index for backward compatibility
|
|
fs.mkdirSync(LEGACY_STORE_DIR, { recursive: true });
|
|
legacyIndex = new LocalIndex(LEGACY_STORE_DIR);
|
|
if (!(await legacyIndex.isIndexCreated())) {
|
|
await legacyIndex.createIndex();
|
|
console.log(`Legacy vector index : created at ${LEGACY_STORE_DIR}`);
|
|
} else {
|
|
console.log(`Legacy vector index : loaded from ${LEGACY_STORE_DIR}`);
|
|
}
|
|
}
|
|
|
|
// Get or create an agent's index
|
|
async function getAgentIndex(agentId) {
|
|
if (!agentId) {
|
|
// No agent specified - use legacy index
|
|
return legacyIndex;
|
|
}
|
|
|
|
if (agentIndexes.has(agentId)) {
|
|
return agentIndexes.get(agentId);
|
|
}
|
|
|
|
const agentDir = path.join(INDEXES_DIR, agentId);
|
|
fs.mkdirSync(agentDir, { recursive: true });
|
|
|
|
const index = new LocalIndex(agentDir);
|
|
if (!(await index.isIndexCreated())) {
|
|
await index.createIndex();
|
|
console.log(`Agent index created : ${agentId} at ${agentDir}`);
|
|
}
|
|
|
|
agentIndexes.set(agentId, index);
|
|
return index;
|
|
}
|
|
|
|
async function add(agentId, { text, tags = [], source = '', vector }) {
|
|
const index = await getAgentIndex(agentId);
|
|
const id = randomUUID();
|
|
const createdAt = new Date().toISOString();
|
|
await index.insertItem({ id, vector, metadata: { id, text, tags, source, createdAt, agentId } });
|
|
return { id, createdAt };
|
|
}
|
|
|
|
async function query(agentId, { vector, limit = 10, tags = [] }) {
|
|
const index = await getAgentIndex(agentId);
|
|
const topK = tags.length ? Math.min(limit * 3, 300) : limit;
|
|
const results = await index.queryItems(vector, topK);
|
|
let items = results;
|
|
if (tags.length) {
|
|
items = results.filter((r) =>
|
|
tags.some((t) => (r.item.metadata.tags || []).includes(t))
|
|
);
|
|
}
|
|
return items.slice(0, limit).map((r) => ({
|
|
...r.item.metadata,
|
|
score: r.score,
|
|
}));
|
|
}
|
|
|
|
async function remove(agentId, id) {
|
|
const index = await getAgentIndex(agentId);
|
|
await index.deleteItem(id);
|
|
}
|
|
|
|
async function list(agentId, { page = 1, pageSize = 50 } = {}) {
|
|
const index = await getAgentIndex(agentId);
|
|
const all = await index.listItems();
|
|
const total = all.length;
|
|
const start = (page - 1) * pageSize;
|
|
const data = all.slice(start, start + pageSize).map((item) => item.metadata);
|
|
return { data, total, page, pageSize, pages: Math.ceil(total / pageSize) };
|
|
}
|
|
|
|
async function count(agentId) {
|
|
if (!agentId) {
|
|
// Return total across all indexes for health check
|
|
let total = 0;
|
|
try {
|
|
const agents = fs.readdirSync(INDEXES_DIR).filter(d => {
|
|
const stat = fs.statSync(path.join(INDEXES_DIR, d));
|
|
return stat.isDirectory();
|
|
});
|
|
for (const agent of agents) {
|
|
const idx = await getAgentIndex(agent);
|
|
const all = await idx.listItems();
|
|
total += all.length;
|
|
}
|
|
} catch {
|
|
// Ignore errors
|
|
}
|
|
return total;
|
|
}
|
|
|
|
const index = await getAgentIndex(agentId);
|
|
const all = await index.listItems();
|
|
return all.length;
|
|
}
|
|
|
|
// Get stats for all agents (master only)
|
|
async function stats() {
|
|
const result = {
|
|
totalAgents: 0,
|
|
totalMemories: 0,
|
|
agents: {}
|
|
};
|
|
|
|
try {
|
|
const agents = fs.readdirSync(INDEXES_DIR).filter(d => {
|
|
const stat = fs.statSync(path.join(INDEXES_DIR, d));
|
|
return stat.isDirectory();
|
|
});
|
|
|
|
result.totalAgents = agents.length;
|
|
|
|
for (const agent of agents) {
|
|
const idx = await getAgentIndex(agent);
|
|
const all = await idx.listItems();
|
|
result.agents[agent] = all.length;
|
|
result.totalMemories += all.length;
|
|
}
|
|
} catch {
|
|
// Ignore errors
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
module.exports = { init, add, query, remove, list, count, stats };
|