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
17 changes: 10 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import type {
FallbackConfig,
} from "./types.js";

function safeParseInt(value: string | undefined, fallback: number): number {
if (!value) return fallback;
const parsed = parseInt(value, 10);
return Number.isNaN(parsed) ? fallback : parsed;
}

const DATA_DIR = join(homedir(), ".agentmemory");
const ENV_FILE = join(DATA_DIR, ".env");

Expand Down Expand Up @@ -71,14 +77,11 @@ export function loadConfig(): AgentMemoryConfig {

return {
engineUrl: env["III_ENGINE_URL"] || "ws://localhost:49134",
restPort: parseInt(env["III_REST_PORT"] || "3111", 10),
streamsPort: parseInt(env["III_STREAMS_PORT"] || "3112", 10),
restPort: parseInt(env["III_REST_PORT"] || "3111", 10) || 3111,
streamsPort: parseInt(env["III_STREAMS_PORT"] || "3112", 10) || 3112,
provider,
tokenBudget: parseInt(env["TOKEN_BUDGET"] || "2000", 10),
maxObservationsPerSession: parseInt(
env["MAX_OBS_PER_SESSION"] || "500",
10,
),
tokenBudget: safeParseInt(env["TOKEN_BUDGET"], 2000),
maxObservationsPerSession: safeParseInt(env["MAX_OBS_PER_SESSION"], 500),
compressionModel: provider.model,
dataDir: DATA_DIR,
};
Expand Down
10 changes: 7 additions & 3 deletions src/functions/auto-forget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,13 @@ export function registerAutoForgetFunction(sdk: ISdk, kv: StateKV): void {
}
}

const latestMemories = memories.filter(
(m) => m.isLatest !== false && !deletedIds.has(m.id),
);
const latestMemories = memories
.filter((m) => m.isLatest !== false && !deletedIds.has(m.id))
.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)
.slice(0, 1000);
for (let i = 0; i < latestMemories.length; i++) {
for (let j = i + 1; j < latestMemories.length; j++) {
const sim = jaccardSimilarity(
Expand Down
2 changes: 1 addition & 1 deletion src/functions/compress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export function registerCompressFunction(
obsId: data.observationId,
error: msg,
});
return { success: false, error: msg };
return { success: false, error: "compression_failed" };
}
},
);
Expand Down
70 changes: 70 additions & 0 deletions src/functions/export-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,76 @@ export function registerExportImportFunction(sdk: ISdk, kv: StateKV): void {
};
}

const MAX_SESSIONS = 10_000;
const MAX_MEMORIES = 50_000;
const MAX_SUMMARIES = 10_000;
const MAX_OBS_PER_SESSION = 5_000;
const MAX_TOTAL_OBSERVATIONS = 500_000;

if (!Array.isArray(importData.sessions)) {
return { success: false, error: "sessions must be an array" };
}
if (!Array.isArray(importData.memories)) {
return { success: false, error: "memories must be an array" };
}
if (!Array.isArray(importData.summaries)) {
return { success: false, error: "summaries must be an array" };
}
if (
typeof importData.observations !== "object" ||
importData.observations === null ||
Array.isArray(importData.observations)
) {
return { success: false, error: "observations must be an object" };
}

if (importData.sessions.length > MAX_SESSIONS) {
return {
success: false,
error: `Too many sessions (max ${MAX_SESSIONS})`,
};
}
if (importData.memories.length > MAX_MEMORIES) {
return {
success: false,
error: `Too many memories (max ${MAX_MEMORIES})`,
};
}
if (importData.summaries.length > MAX_SUMMARIES) {
return {
success: false,
error: `Too many summaries (max ${MAX_SUMMARIES})`,
};
}
const MAX_OBS_BUCKETS = 10_000;
const obsBuckets = Object.keys(importData.observations);
if (obsBuckets.length > MAX_OBS_BUCKETS) {
return {
success: false,
error: `Too many observation buckets (max ${MAX_OBS_BUCKETS})`,
};
}

let totalObservations = 0;
for (const [, obs] of Object.entries(importData.observations)) {
if (!Array.isArray(obs)) {
return { success: false, error: "observation values must be arrays" };
}
if (obs.length > MAX_OBS_PER_SESSION) {
return {
success: false,
error: `Too many observations per session (max ${MAX_OBS_PER_SESSION})`,
};
}
totalObservations += obs.length;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (totalObservations > MAX_TOTAL_OBSERVATIONS) {
return {
success: false,
error: `Too many total observations (max ${MAX_TOTAL_OBSERVATIONS})`,
};
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const stats = {
sessions: 0,
observations: 0,
Expand Down
7 changes: 2 additions & 5 deletions src/functions/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import type {
SessionSummary,
} from "../types.js";

const ALLOWED_DIRS = [
resolve(homedir(), ".agentmemory"),
resolve(homedir(), ".claude"),
];
const ALLOWED_DIRS = [resolve(homedir(), ".agentmemory")];

function isAllowedPath(dbPath: string): boolean {
const resolved = resolve(dbPath);
Expand Down Expand Up @@ -149,7 +146,7 @@ export function registerMigrateFunction(sdk: ISdk, kv: StateKV): void {
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
ctx.logger.error("Migration failed", { error: msg });
return { success: false, error: msg };
return { success: false, error: "Migration failed" };
}
},
);
Expand Down
16 changes: 16 additions & 0 deletions src/functions/observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ export function registerObserveFunction(
},
async (payload: HookPayload) => {
const ctx = getContext();

if (
!payload?.sessionId ||
typeof payload.sessionId !== "string" ||
!payload.hookType ||
typeof payload.hookType !== "string" ||
!payload.timestamp ||
typeof payload.timestamp !== "string"
) {
return {
success: false,
error:
"Invalid payload: sessionId, hookType, and timestamp are required",
};
}

const obsId = generateId("obs");

if (dedupMap) {
Expand Down
28 changes: 17 additions & 11 deletions src/functions/privacy.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
import type { ISdk } from 'iii-sdk'
import type { ISdk } from "iii-sdk";

const PRIVATE_TAG_RE = /<private>[\s\S]*?<\/private>/gi
const PRIVATE_TAG_RE = /<private>[\s\S]*?<\/private>/gi;

const SECRET_PATTERN_SOURCES = [
/(?:api[_-]?key|secret|token|password|credential|auth)[\s]*[=:]\s*["']?[A-Za-z0-9_\-/.+]{20,}["']?/gi,
/(?:sk|pk|rk|ak)-[A-Za-z0-9]{20,}/g,
/sk-ant-[A-Za-z0-9\-_]{20,}/g,
/ghp_[A-Za-z0-9]{36}/g,
/github_pat_[A-Za-z0-9_]{22,}/g,
/xoxb-[A-Za-z0-9\-]+/g,
/AKIA[0-9A-Z]{16}/g,
/AIza[A-Za-z0-9\-_]{35}/g,
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
]
];

export function stripPrivateData(input: string): string {
let result = input.replace(PRIVATE_TAG_RE, '[REDACTED]')
let result = input.replace(PRIVATE_TAG_RE, "[REDACTED]");
for (const source of SECRET_PATTERN_SOURCES) {
const pattern = new RegExp(source.source, source.flags)
result = result.replace(pattern, '[REDACTED_SECRET]')
const pattern = new RegExp(source.source, source.flags);
result = result.replace(pattern, "[REDACTED_SECRET]");
}
return result
return result;
}

export function registerPrivacyFunction(sdk: ISdk): void {
sdk.registerFunction(
{ id: 'mem::privacy', description: 'Strip private tags and secrets from input' },
{
id: "mem::privacy",
description: "Strip private tags and secrets from input",
},
async (data: { input: string }) => {
return { output: stripPrivateData(data.input) }
}
)
return { output: stripPrivateData(data.input) };
},
);
}
14 changes: 8 additions & 6 deletions src/functions/relations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,20 @@ export function registerRelationsFunction(sdk: ISdk, kv: StateKV): void {
},
async (data: { memoryId: string; maxHops?: number }) => {
const ctx = getContext();
const maxHops = data.maxHops ?? 2;
const maxHops = Math.min(data.maxHops ?? 2, 5);
const MAX_VISITED = 500;

const allRelations = await kv
.list<MemoryRelation>(KV.relations)
.catch(() => []);

const visited = new Set<string>();
const result: Array<{ memory: Memory; hop: number }> = [];
const queue: Array<{ id: string; hop: number }> = [
{ id: data.memoryId, hop: 0 },
];

while (queue.length > 0) {
while (queue.length > 0 && visited.size < MAX_VISITED) {
const current = queue.shift()!;
if (visited.has(current.id) || current.hop > maxHops) continue;
visited.add(current.id);
Expand All @@ -136,10 +141,7 @@ export function registerRelationsFunction(sdk: ISdk, kv: StateKV): void {
const supersedes = memory.supersedes || [];
const parentId = memory.parentId ? [memory.parentId] : [];

const kvRelations = await kv
.list<MemoryRelation>(KV.relations)
.catch(() => []);
const kvLinked = kvRelations
const kvLinked = allRelations
.filter((r) => r.sourceId === current.id || r.targetId === current.id)
.map((r) => (r.sourceId === current.id ? r.targetId : r.sourceId));

Expand Down
12 changes: 11 additions & 1 deletion src/functions/remember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,19 @@ export function registerRememberFunction(sdk: ISdk, kv: StateKV): void {
files?: string[];
}) => {
const ctx = getContext();
if (!data.content || !data.content.trim()) {
if (
!data.content ||
typeof data.content !== "string" ||
!data.content.trim()
) {
return { success: false, error: "content is required" };
}
if (data.files && !Array.isArray(data.files)) {
return { success: false, error: "files must be an array" };
}
if (data.concepts && !Array.isArray(data.concepts)) {
return { success: false, error: "concepts must be an array" };
}
const validTypes = new Set([
"pattern",
"preference",
Expand Down
10 changes: 7 additions & 3 deletions src/functions/smart-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ export function registerSmartSearchFunction(
const ctx = getContext();

if (data.expandIds && data.expandIds.length > 0) {
const ids = data.expandIds.slice(0, 20);
const expanded: Array<{
obsId: string;
sessionId: string;
observation: CompressedObservation;
}> = [];

for (const obsId of data.expandIds) {
for (const obsId of ids) {
const obs = await findObservation(kv, obsId);
if (obs) {
expanded.push({
Expand All @@ -40,11 +41,14 @@ export function registerSmartSearchFunction(
}
}

const truncated = data.expandIds.length > ids.length;
ctx.logger.info("Smart search expanded", {
requested: data.expandIds.length,
found: expanded.length,
attempted: ids.length,
returned: expanded.length,
truncated,
});
return { mode: "expanded", results: expanded };
return { mode: "expanded", results: expanded, truncated };
}

if (!data.query || typeof data.query !== "string" || !data.query.trim()) {
Expand Down
22 changes: 0 additions & 22 deletions src/health/recovery.ts

This file was deleted.

23 changes: 12 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,19 +140,20 @@ async function main() {
console.warn(`[agentmemory] Failed to load persisted index:`, err);
return null;
});
if (loaded?.bm25) {
const restoredCount = loaded.bm25.size;
if (restoredCount > 0) {
console.log(
`[agentmemory] Loaded persisted BM25 index (${restoredCount} docs)`,
);
}
if (loaded?.bm25 && loaded.bm25.size > 0) {
bm25Index.restoreFrom(loaded.bm25);
console.log(
`[agentmemory] Loaded persisted BM25 index (${bm25Index.size} docs)`,
);
}
if (loaded?.vector && vectorIndex && loaded.vector.size > 0) {
vectorIndex.restoreFrom(loaded.vector);
console.log(
`[agentmemory] Loaded persisted vector index (${vectorIndex.size} vectors)`,
);
}

const needsRebuild =
!loaded?.bm25 ||
loaded.bm25.size === 0 ||
(embeddingProvider && vectorIndex && vectorIndex.size === 0);
const needsRebuild = bm25Index.size === 0;

if (needsRebuild) {
const indexCount = await rebuildIndex(kv).catch((err) => {
Expand Down
Loading