Skip to content
Open
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
4 changes: 2 additions & 2 deletions apps/memos-local-openclaw/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export function initPlugin(opts: PluginInitOptions = {}): MemosLocalPlugin {

const tools: ToolDefinition[] = [
createMemorySearchTool(engine, store, ctx, sharedState),
createMemoryTimelineTool(store),
createMemoryGetTool(store),
createMemoryTimelineTool(store, workspaceDir),
createMemoryGetTool(store, workspaceDir),
createNetworkMemoryDetailTool(store, ctx),
];

Expand Down
11 changes: 4 additions & 7 deletions apps/memos-local-openclaw/src/tools/memory-get.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import type { SqliteStore } from "../storage/sqlite";
import type { ToolDefinition, GetResult, ChunkRef } from "../types";
import { DEFAULTS } from "../types";
import { resolveDefaultOwner, resolveOwnerFilter } from "./resolve-owner";

function resolveOwnerFilter(owner: unknown): string[] {
const resolvedOwner = typeof owner === "string" && owner.trim().length > 0 ? owner : "agent:main";
return resolvedOwner === "public" ? ["public"] : [resolvedOwner, "public"];
}

export function createMemoryGetTool(store: SqliteStore): ToolDefinition {
export function createMemoryGetTool(store: SqliteStore, workspaceDir?: string): ToolDefinition {
const defaultOwner = resolveDefaultOwner(workspaceDir);
return {
name: "memory_get",
description:
Expand Down Expand Up @@ -41,7 +38,7 @@ export function createMemoryGetTool(store: SqliteStore): ToolDefinition {
DEFAULTS.getMaxCharsMax,
);

const chunk = store.getChunksByRef(ref, resolveOwnerFilter(input.owner));
const chunk = store.getChunksByRef(ref, resolveOwnerFilter(input.owner, defaultOwner));

if (!chunk) {
return { error: `Chunk not found: ${ref.chunkId}` };
Expand Down
9 changes: 3 additions & 6 deletions apps/memos-local-openclaw/src/tools/memory-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import type { HubScope, HubSearchResult } from "../sharing/types";
import type { RecallEngine } from "../recall/engine";
import type { PluginContext, ToolDefinition } from "../types";
import type { SqliteStore } from "../storage/sqlite";

function resolveOwnerFilter(owner: unknown): string[] {
const resolvedOwner = typeof owner === "string" && owner.trim().length > 0 ? owner : "agent:main";
return resolvedOwner === "public" ? ["public"] : [resolvedOwner, "public"];
}
import { resolveDefaultOwner, resolveOwnerFilter } from "./resolve-owner";

function resolveScope(scope: unknown): HubScope {
return scope === "group" || scope === "all" ? scope : "local";
Expand All @@ -25,6 +21,7 @@ function emptyHubResult(scope: HubScope): HubSearchResult {
}

export function createMemorySearchTool(engine: RecallEngine, store?: SqliteStore, ctx?: PluginContext, sharedState?: { lastSearchTime: number }): ToolDefinition {
const defaultOwner = resolveDefaultOwner(ctx?.workspaceDir);
return {
name: "memory_search",
description:
Expand Down Expand Up @@ -64,7 +61,7 @@ export function createMemorySearchTool(engine: RecallEngine, store?: SqliteStore
const query = (input.query as string) ?? "";
const maxResults = input.maxResults as number | undefined;
const minScore = input.minScore as number | undefined;
const ownerFilter = resolveOwnerFilter(input.owner);
const ownerFilter = resolveOwnerFilter(input.owner, defaultOwner);
const scope = resolveScope(input.scope);

const localSearch = engine.search({
Expand Down
11 changes: 4 additions & 7 deletions apps/memos-local-openclaw/src/tools/memory-timeline.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import type { SqliteStore } from "../storage/sqlite";
import type { ToolDefinition, TimelineResult, TimelineEntry, ChunkRef } from "../types";
import { DEFAULTS } from "../types";
import { resolveDefaultOwner, resolveOwnerFilter } from "./resolve-owner";

function resolveOwnerFilter(owner: unknown): string[] {
const resolvedOwner = typeof owner === "string" && owner.trim().length > 0 ? owner : "agent:main";
return resolvedOwner === "public" ? ["public"] : [resolvedOwner, "public"];
}

export function createMemoryTimelineTool(store: SqliteStore): ToolDefinition {
export function createMemoryTimelineTool(store: SqliteStore, workspaceDir?: string): ToolDefinition {
const defaultOwner = resolveDefaultOwner(workspaceDir);
return {
name: "memory_timeline",
description:
Expand Down Expand Up @@ -38,7 +35,7 @@ export function createMemoryTimelineTool(store: SqliteStore): ToolDefinition {
const ref = input.ref as ChunkRef;
const window = (input.window as number) ?? DEFAULTS.timelineWindowDefault;

const ownerFilter = resolveOwnerFilter(input.owner);
const ownerFilter = resolveOwnerFilter(input.owner, defaultOwner);
const anchorChunk = store.getChunksByRef(ref, ownerFilter);
if (!anchorChunk) {
return { entries: [], anchorRef: ref } satisfies TimelineResult;
Expand Down
30 changes: 30 additions & 0 deletions apps/memos-local-openclaw/src/tools/resolve-owner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as path from "path";

/**
* Derive the default owner from the workspace directory name.
*
* Convention: a workspace directory named "workspace-<agentName>" maps to
* "agent:<agentName>". If the directory name does not follow that pattern
* (or no workspaceDir is provided) the fallback is "agent:main".
*/
export function resolveDefaultOwner(workspaceDir?: string): string {
if (workspaceDir) {
const base = path.basename(workspaceDir);
const match = base.match(/^workspace-(.+)$/);
if (match) {
return `agent:${match[1]}`;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

resolveDefaultOwner returns agent:${match[1]} directly from the directory name capture. If the workspace directory name has leading/trailing whitespace (or other unexpected characters), this can create an owner value that will never match DB rows. Consider trimming/sanitizing the captured agent name (and optionally constraining it to an allowed character set) before building the owner string.

Suggested change
return `agent:${match[1]}`;
const agentName = match[1].trim();
if (agentName.length > 0) {
return `agent:${agentName}`;
}

Copilot uses AI. Check for mistakes.
}
}
return "agent:main";
}
Comment on lines +3 to +19
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Owner default resolution is now central to multi-agent isolation behavior, but there are no tests covering the new workspace-<name>agent:<name> mapping (including fallback to agent:main and explicit owner precedence). Adding a small unit test for resolveDefaultOwner/resolveOwnerFilter would prevent regressions.

Copilot uses AI. Check for mistakes.

/**
* Build the owner filter array used for queries.
*
* If the caller supplied an explicit `owner` value it takes precedence;
* otherwise the `defaultOwner` (derived from workspace context) is used.
*/
export function resolveOwnerFilter(owner: unknown, defaultOwner: string = "agent:main"): string[] {
const resolvedOwner = typeof owner === "string" && owner.trim().length > 0 ? owner : defaultOwner;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

resolveOwnerFilter checks owner.trim().length > 0 but then uses the untrimmed owner value. This means inputs like "public " or "agent:dev\n" will produce an ownerFilter that won’t match stored owners (and "public " won’t be treated as the special public case). Consider normalizing to the trimmed string before comparisons/returning the filter.

Suggested change
const resolvedOwner = typeof owner === "string" && owner.trim().length > 0 ? owner : defaultOwner;
const trimmedOwner = typeof owner === "string" ? owner.trim() : "";
const resolvedOwner = trimmedOwner.length > 0 ? trimmedOwner : defaultOwner;

Copilot uses AI. Check for mistakes.
return resolvedOwner === "public" ? ["public"] : [resolvedOwner, "public"];
}
Loading