Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 0 additions & 1 deletion scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,3 @@ function main() {
}

main();
// AUDIT_MARKER
1 change: 0 additions & 1 deletion scripts/migrate-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,3 @@ function migrateDirectory(dir) {
}

migrateDirectory("tests");
// AUDIT_MARKER
1 change: 0 additions & 1 deletion scripts/migrate-v1-to-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,4 +405,3 @@ migrate(storagePath)
console.error("Migration failed:", error);
process.exit(1);
});
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,4 +751,3 @@ export function isConfigured(): boolean {
// The system should check specific sub-configurations when features are invoked.
return true;
}
// AUDIT_MARKER
13 changes: 6 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
}
})();

if (CONFIG.webServerEnabled) {

Check failure on line 69 in src/index.ts

View workflow job for this annotation

GitHub Actions / check

TypeError: undefined is not an object (evaluating 'CONFIG.webServerEnabled')

at <anonymous> (/home/runner/work/opencode-mem0/opencode-mem0/src/index.ts:69:7) at async <anonymous> (/home/runner/work/opencode-mem0/opencode-mem0/tests/tool-scope.test.ts:165:26)

Check failure on line 69 in src/index.ts

View workflow job for this annotation

GitHub Actions / check

TypeError: undefined is not an object (evaluating 'CONFIG.webServerEnabled')

at <anonymous> (/home/runner/work/opencode-mem0/opencode-mem0/src/index.ts:69:7) at async <anonymous> (/home/runner/work/opencode-mem0/opencode-mem0/tests/tool-scope.test.ts:156:26)

Check failure on line 69 in src/index.ts

View workflow job for this annotation

GitHub Actions / check

TypeError: undefined is not an object (evaluating 'CONFIG.webServerEnabled')

at <anonymous> (/home/runner/work/opencode-mem0/opencode-mem0/src/index.ts:69:7) at async <anonymous> (/home/runner/work/opencode-mem0/opencode-mem0/tests/tool-scope.test.ts:147:26)
startWebServer({
port: CONFIG.webServerPort,
host: CONFIG.webServerHost,
Expand All @@ -88,7 +88,7 @@
duration: 3000,
},
})
.catch(() => {});
.catch((err) => log("Toast display failed", { error: String(err) }));
}
});

Expand All @@ -103,7 +103,7 @@
duration: 5000,
},
})
.catch(() => {});
.catch((err) => log("Toast display failed", { error: String(err) }));
}
} else {
if (ctx.client?.tui) {
Expand All @@ -116,7 +116,7 @@
duration: 3000,
},
})
.catch(() => {});
.catch((err) => log("Toast display failed", { error: String(err) }));
}
}
})
Expand All @@ -133,7 +133,7 @@
duration: 5000,
},
})
.catch(() => {});
.catch((err) => log("Toast display failed", { error: String(err) }));
}
});
}
Expand Down Expand Up @@ -278,7 +278,7 @@
duration: 5000,
},
})
.catch(() => {});
.catch((err) => log("Toast display failed", { error: String(err) }));
}
}
},
Expand Down Expand Up @@ -587,7 +587,7 @@
duration: 3000,
},
})
.catch(() => {});
.catch((err) => log("Toast display failed", { error: String(err) }));
}

log("Compaction memory injected", {
Expand Down Expand Up @@ -634,4 +634,3 @@

return output;
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ export const id =
typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : "opencode-mem0";
export { OpenCodeMemPlugin };
export default { id, server: OpenCodeMemPlugin } satisfies PluginModule;
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/ai/ai-provider-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,3 @@ export class AIProviderFactory {
return getAISessionManager().cleanupExpiredSessions();
}
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/ai/opencode-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,3 @@ export async function generateStructuredOutput<T>(options: {
});
return result.output as T;
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/ai/provider-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,3 @@ export function buildMemoryProviderConfig(
iterationTimeout: overrides.iterationTimeout ?? config.autoCaptureIterationTimeout,
};
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/ai/providers/anthropic-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,3 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
return null;
}
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/ai/providers/base-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,3 @@ export abstract class BaseAIProvider {

abstract supportsSession(): boolean;
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/ai/providers/google-gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,3 @@ export class GoogleGeminiProvider extends BaseAIProvider {
return { success: false, error: `Max iterations (${maxIterations}) reached`, iterations };
}
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/ai/providers/openai-chat-completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,4 +434,3 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
};
}
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/ai/providers/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,3 @@ export class OpenAIResponsesProvider extends BaseAIProvider {
return data;
}
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/ai/session/ai-session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,3 @@ export const aiSessionManager = {
return getAISessionManager().getLastSequence.bind(getAISessionManager());
},
} as unknown as AISessionManager;
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/ai/session/session-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,3 @@ export interface SessionUpdateParams {
conversationId?: string;
metadata?: Record<string, any>;
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/ai/tools/tool-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,3 @@ export class ToolSchemaConverter {
};
}
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/ai/validators/user-profile-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,3 @@ export class UserProfileValidator {
return errors;
}
}
// AUDIT_MARKER
67 changes: 34 additions & 33 deletions src/services/api-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,15 @@ export async function handleListMemories(
includePrompts: boolean = true
): Promise<ApiResponse<PaginatedResponse<Memory | any>>> {
try {
const safePage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
const safePageSize =
Number.isFinite(pageSize) && pageSize > 0 && pageSize <= 100 ? Math.floor(pageSize) : 20;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bound the page window before using it as a shard read limit.

safePageSize is capped, but safePage is not. On Line 153, a very large page can still turn listMemories into an effectively unbounded read and materialize far more rows than this endpoint can safely return. Cap or reject oversized page windows before calling storage.

Suggested fix
   try {
     const safePage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
     const safePageSize =
       Number.isFinite(pageSize) && pageSize > 0 && pageSize <= 100 ? Math.floor(pageSize) : 20;
+    const requestedWindow = safePage * safePageSize;
+    if (requestedWindow > 500) {
+      return { success: false, error: "Requested page window is too large" };
+    }
     await embeddingService.warmup();
     let allMemories: any[] = [];
     if (tag) {
       const { scope: tagScope, hash } = extractScopeFromTag(tag);
       const shards = shardManager.getAllShards(tagScope, hash);
-      const limit = safePage * safePageSize; // Fetch enough to cover the requested page across shards
+      const limit = requestedWindow;

Also applies to: 153-153

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/api-handlers.ts` around lines 145 - 147, The code caps
safePageSize but not safePage, so a huge page value can be passed into
listMemories and cause an effectively unbounded read; modify the request
handling that computes safePage to constrain the overall window (e.g., enforce a
maxPage and/or maxOffset = maxPage * safePageSize) or reject requests where page
> MAX_PAGE before calling listMemories; update the logic around
safePage/safePageSize and the call to listMemories to use the bounded page or
computed maxOffset (reference safePage, safePageSize, listMemories) so storage
is never asked to read beyond the allowed window.

await embeddingService.warmup();
let allMemories: any[] = [];
if (tag) {
const { scope: tagScope, hash } = extractScopeFromTag(tag);
const shards = shardManager.getAllShards(tagScope, hash);
const limit = page * pageSize; // Fetch enough to cover the requested page across shards
const limit = safePage * safePageSize; // Fetch enough to cover the requested page across shards
for (const shard of shards) {
const db = connectionManager.getConnection(shard.dbPath);
const memories = vectorSearch.listMemories(db, tag, limit);
Expand Down Expand Up @@ -233,9 +236,9 @@ export async function handleListMemories(
timeline = sortedTimeline;

const total = timeline.length;
const totalPages = Math.ceil(total / pageSize);
const offset = (page - 1) * pageSize;
const paginatedResults = timeline.slice(offset, offset + pageSize);
const totalPages = Math.ceil(total / safePageSize);
const offset = (safePage - 1) * safePageSize;
const paginatedResults = timeline.slice(offset, offset + safePageSize);

const items = paginatedResults.map((item: any) => {
if (item.type === "memory") {
Expand Down Expand Up @@ -491,6 +494,9 @@ export async function handleSearch(
): Promise<ApiResponse<PaginatedResponse<SearchResultItem>>> {
try {
if (!query) return { success: false, error: "query is required" };
const safePage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
const safePageSize =
Number.isFinite(pageSize) && pageSize > 0 && pageSize <= 100 ? Math.floor(pageSize) : 20;
await embeddingService.warmup();
const queryVector = await embeddingService.embedWithTimeout(query);
let memoryResults: any[] = [];
Expand All @@ -500,39 +506,32 @@ export async function handleSearch(
const shards = shardManager.getAllShards(scope, hash);
for (const shard of shards) {
try {
const results = await vectorSearch.searchInShard(shard, queryVector, tag, pageSize * 2);
const results = await vectorSearch.searchInShard(
shard,
queryVector,
tag,
safePageSize * 2
);
memoryResults.push(...results);
} catch (error) {
log("Shard search error", { shardId: shard.id, error: String(error) });
}
}
const projectPath = getProjectPathFromTag(tag);
promptResults = userPromptManager.searchPrompts(query, projectPath, pageSize * 2);
promptResults = userPromptManager.searchPrompts(query, projectPath, safePageSize * 2);
} else {
const userShards = shardManager.getAllShards("user", "");
const projectShards = shardManager.getAllShards("project", "");
const uniqueTags = new Set<string>();
for (const shard of projectShards) {
const db = connectionManager.getConnection(shard.dbPath);
const tags = vectorSearch.getDistinctTags(db);
for (const t of tags) {
if (t.container_tag) uniqueTags.add(t.container_tag);
}
}
for (const containerTag of uniqueTags) {
const { scope, hash } = extractScopeFromTag(containerTag);
const shards = shardManager.getAllShards(scope, hash);
for (const shard of shards) {
try {
const results = await vectorSearch.searchInShard(
shard,
queryVector,
containerTag,
pageSize
);
memoryResults.push(...results);
} catch (error) {
log("Shard search error", { shardId: shard.id, error: String(error) });
}
const searchedPaths = new Set<string>();
for (const shard of [...userShards, ...projectShards]) {
Comment on lines +526 to +529

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep untagged web searches scoped to project shards

When the web UI searches with no selected tag, it calls /api/search without tag from the project memory view, and handleListMemories/LocalMemoryClient both treat the untagged/all-projects case as project-only. Adding userShards here means any migrated mem_user_* memories are now searched with an empty containerTag and returned in that project UI, exposing personal/user-scope memories and making them selectable for project memory actions. Please keep this branch to project shards unless the API/UI explicitly requests a user scope.

Useful? React with 👍 / 👎.

if (searchedPaths.has(shard.dbPath)) continue;
searchedPaths.add(shard.dbPath);
try {
const perShardLimit = Math.min(safePage * safePageSize, 500);
const results = await vectorSearch.searchInShard(shard, queryVector, "", perShardLimit);
memoryResults.push(...results);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} catch (error) {
log("Shard search error", { shardId: shard.id, error: String(error) });
}
}
promptResults = userPromptManager.searchPrompts(query, undefined, pageSize * 2);
Expand Down Expand Up @@ -575,9 +574,12 @@ export async function handleSearch(
);

const total = combinedResults.length;
const totalPages = Math.ceil(total / pageSize);
const offset = (page - 1) * pageSize;
const paginatedResults: SearchResultItem[] = combinedResults.slice(offset, offset + pageSize);
const totalPages = Math.ceil(total / safePageSize);
const offset = (safePage - 1) * safePageSize;
const paginatedResults: SearchResultItem[] = combinedResults.slice(
offset,
offset + safePageSize
);

const missingPromptIds = new Set<string>();
const missingMemoryIds = new Set<string>();
Expand Down Expand Up @@ -1179,4 +1181,3 @@ export async function handleConflictStats(): Promise<
return { success: false, error: "Internal error" };
}
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/auto-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,4 +393,3 @@ Analyze this conversation. If it contains technical work (code, bugs, features,
tags: (result.data.tags || []).map((t: string) => t.toLowerCase().trim()),
};
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/cleanup-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,3 @@ export class CleanupService {
}

export const cleanupService = new CleanupService();
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,4 +444,3 @@ export class LocalMemoryClient {
}

export const memoryClient = new LocalMemoryClient();
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,3 @@ export function formatContextForPrompt(

return parts.join("\n");
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/deduplication-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,3 @@ export class DeduplicationService {
}

export const deduplicationService = new DeduplicationService();
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/embedding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,3 @@ export class EmbeddingService {
}

export const embeddingService = EmbeddingService.getInstance();
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/jsonc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,3 @@ export function stripJsoncComments(content: string): string {
// Remove trailing commas before } or ]
return result.replace(/,\s*([}\]])/g, "$1");
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/language-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,3 @@ export function getLanguageName(code: string): string {
}
return lang?.name || "English";
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/memory-conflicts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,4 +654,3 @@ export function getAllUnresolvedConflicts(

return allConflicts.sort((a, b) => b.detectedAt - a.detectedAt).slice(0, limit);
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/memory-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,3 @@ export async function runLifecycleMaintenance(): Promise<void> {
log("Lifecycle maintenance error", { error: String(error) });
}
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/memory-scoring-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,3 @@ export async function runOneTimeScoringRecalculation(): Promise<{
}> {
return recalculateAllScores(true);
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/memory-scoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,4 +619,3 @@ export function recordAccess(currentAccessCount: number): {
lastAccessed: Date.now(),
};
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/migration-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,4 +350,3 @@ export class MigrationService {
}

export const migrationService = new MigrationService();
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/platform-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,3 @@ export function serve(options: ServeOptions): Promise<PlatformServer> {

return createNodeServer(options);
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/privacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ export function isFullyPrivate(content: string): boolean {
const stripped = stripPrivateContent(content).trim();
return stripped === "[REDACTED]" || stripped === "";
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/retrieval-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,3 @@ export function calculateDiversityPenalty(

return 0;
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/secret-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,3 @@ export function resolveSecretValue(value: string | undefined): string | undefine

return value;
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/sqlite/connection-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,3 @@ export class ConnectionManager {
}

export const connectionManager = new ConnectionManager();
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/sqlite/shard-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,4 +405,3 @@ export class ShardManager {
}

export const shardManager = new ShardManager();
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/sqlite/sqlite-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,3 @@ export function getDatabase(): new (path: string) => Database {
}
return DatabaseImpl;
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/sqlite/transcript-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,3 @@ export class TranscriptManager {
}

export const transcriptManager = new TranscriptManager();
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/sqlite/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,3 @@ export interface SearchResult {
contextBoost?: number;
finalScore?: number;
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/sqlite/vector-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,4 +675,3 @@ export class VectorSearch {
}

export const vectorSearch = new VectorSearch();
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,3 @@ export function getTags(directory: string): {
project: getProjectTagInfo(directory),
};
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/transcript-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,3 @@ export async function cleanupOldTranscripts(): Promise<number> {

return transcriptManager.deleteOldTranscripts(cutoffTime);
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/user-memory-learning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,3 @@ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "ne

return rawData as UserProfileData;
}
// AUDIT_MARKER
1 change: 0 additions & 1 deletion src/services/user-profile/profile-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,3 @@ export function getUserProfileContext(userId: string): string | null {

return parts.join("\n");
}
// AUDIT_MARKER
Loading
Loading