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
2 changes: 1 addition & 1 deletion dist/index.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

37 changes: 33 additions & 4 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "cortex",
"name": "Memory (Cortex)",
"description": "Cortex memory engine retrieval, storage, and lifecycle management for evaOS",
"description": "Cortex memory engine \u2014 retrieval, storage, and lifecycle management for evaOS",
"version": "1.0.0",
"kind": "memory",
"main": "dist/index.js",
Expand All @@ -15,6 +15,7 @@
"cortex_add_commitment",
"cortex_update_commitment",
"cortex_list_commitments",
"cortex_insights",
"cortex_add_open_loop",
"cortex_resolve_open_loop",
"cortex_list_open_loops"
Expand Down Expand Up @@ -45,7 +46,7 @@
},
"shadowMode": {
"type": "boolean",
"description": "Shadow mode capture runs extraction but skips storage (dry-run, default: false)"
"description": "Shadow mode \u2014 capture runs extraction but skips storage (dry-run, default: false)"
},
"retrievalBudget": {
"type": "number",
Expand All @@ -57,10 +58,38 @@
},
"retrievalMode": {
"type": "string",
"enum": ["auto", "fast", "thorough"],
"enum": [
"auto",
"fast",
"thorough"
],
"description": "Retrieval mode for memory search (default: fast)"
},
"injectionFormat": {
"type": "string",
"enum": [
"v1",
"v2"
],
"default": "v1",
"description": "Memory injection format version"
},
"showConflicts": {
"type": "boolean",
"default": true,
"description": "Show conflict markers in v2 format"
},
"showRelations": {
"type": "boolean",
"default": true,
"description": "Show relation hints in v2 format"
},
"dedup": {
"type": "boolean",
"default": true,
"description": "Deduplicate similar memories in v2 format"
Comment on lines +68 to +90
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The PR description mentions exposing the new cortex_insights tool in the manifest, but this diff also adds new config schema properties (injectionFormat, showConflicts, showRelations, dedup). If those additions are intentional, please update the PR description to reflect the broader manifest/config change so reviewers/users know about the new config options.

Copilot uses AI. Check for mistakes.
}
Comment on lines +67 to 91
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Config schema in openclaw.plugin.json catches up to already-shipped code

The new config properties (injectionFormat, showConflicts, showRelations, dedup) added to openclaw.plugin.json are already present in the EvaMemoryConfig interface and parseConfig at src/index.ts:55-58 and src/index.ts:122-125, shipped in the prior PR #10 (feat: add v2 memory injection formatting). This PR is simply aligning the JSON schema declaration with the already-functional code, which is a good catch-up.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

},
"required": []
}
}
}
45 changes: 45 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,14 @@ class CortexClient {
return this.get<{ commitments?: Record<string, unknown>[]; total?: number }>(`/api/v1/commitments?${params}`);
}

// --- Insights ---

async listInsights(status = "pending", limit = 5) {
return this.get<{ insights?: Record<string, unknown>[]; count?: number }>(
`/api/v1/insights?owner_id=${encodeURIComponent(this.ownerId)}&status=${encodeURIComponent(status)}&limit=${encodeURIComponent(String(limit))}`,
Comment on lines +364 to +365
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

listInsights builds the query string manually, unlike the other list* methods that use URLSearchParams. To keep encoding consistent and avoid subtle query-string bugs as parameters evolve, build the URL with new URLSearchParams({ owner_id: ..., status: ..., limit: ... }) and interpolate ?${params} (similar to listCommitments / listOpenLoops).

Suggested change
return this.get<{ insights?: Record<string, unknown>[]; count?: number }>(
`/api/v1/insights?owner_id=${encodeURIComponent(this.ownerId)}&status=${encodeURIComponent(status)}&limit=${encodeURIComponent(String(limit))}`,
const params = new URLSearchParams({
owner_id: this.ownerId,
status,
limit: String(limit),
});
return this.get<{ insights?: Record<string, unknown>[]; count?: number }>(
`/api/v1/insights?${params}`,

Copilot uses AI. Check for mistakes.
Comment on lines +363 to +365
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

listInsights is the only list-style CortexClient method here that does not accept an optional ownerId parameter (the other list methods do). If insights are namespaced like the other resources, consider adding ownerId?: string and defaulting to this.ownerId for API consistency and future flexibility.

Suggested change
async listInsights(status = "pending", limit = 5) {
return this.get<{ insights?: Record<string, unknown>[]; count?: number }>(
`/api/v1/insights?owner_id=${encodeURIComponent(this.ownerId)}&status=${encodeURIComponent(status)}&limit=${encodeURIComponent(String(limit))}`,
async listInsights(ownerId?: string, status = "pending", limit = 5) {
return this.get<{ insights?: Record<string, unknown>[]; count?: number }>(
`/api/v1/insights?owner_id=${encodeURIComponent(ownerId ?? this.ownerId)}&status=${encodeURIComponent(status)}&limit=${encodeURIComponent(String(limit))}`,

Copilot uses AI. Check for mistakes.
);
}
Comment on lines +363 to +367
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: listInsights uses manual URL encoding instead of URLSearchParams

The new listInsights method at src/index.ts:363-367 manually constructs the query string using encodeURIComponent, whereas all other list methods (listCommitments at src/index.ts:355-358, listOpenLoops at src/index.ts:385-388, listContradictions at src/index.ts:325-329) use URLSearchParams. Both approaches produce correct output, but the inconsistency makes the codebase slightly harder to maintain. Not a bug, but a style divergence worth noting.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


// --- Open Loops ---

async addOpenLoop(description: string, ownerId?: string) {
Expand Down Expand Up @@ -1699,6 +1707,43 @@ const cortexPlugin = {
{ name: "cortex_list_commitments" },
);

api.registerTool(
{
name: "cortex_insights",
label: "Cortex Insights",
description: "List cross-system insights from behavioral + memory analysis. Shows patterns, correlations, and recommendations discovered by the dreaming engine.",
parameters: Type.Object({
status: Type.Optional(Type.String({ description: "Filter by status: pending, accepted, or all" })),
limit: Type.Optional(Type.Number({ description: "Max results to return (default: 5)" })),
}),
async execute(_toolCallId: string, params: unknown): Promise<any> {
const { status, limit } = params as { status?: string; limit?: number };
try {
const result = await client.listInsights(status ?? "pending", limit ?? 5);
Comment on lines +1716 to +1722
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The tool schema allows any string status and any numeric limit (including negative / non-integer / very large values). Since these directly control the API query and output size, constrain them in the TypeBox schema (e.g., literals/union for status, and an integer with min/max for limit) and/or clamp/validate before calling client.listInsights.

Suggested change
status: Type.Optional(Type.String({ description: "Filter by status: pending, accepted, or all" })),
limit: Type.Optional(Type.Number({ description: "Max results to return (default: 5)" })),
}),
async execute(_toolCallId: string, params: unknown): Promise<any> {
const { status, limit } = params as { status?: string; limit?: number };
try {
const result = await client.listInsights(status ?? "pending", limit ?? 5);
status: Type.Optional(
Type.Union(
[Type.Literal("pending"), Type.Literal("accepted"), Type.Literal("all")],
{ description: "Filter by status: pending, accepted, or all" },
),
),
limit: Type.Optional(
Type.Integer({ minimum: 1, maximum: 50, description: "Max results to return (default: 5, max: 50)" }),
),
}),
async execute(_toolCallId: string, params: unknown): Promise<any> {
const { status, limit } = params as {
status?: "pending" | "accepted" | "all";
limit?: number;
};
try {
const safeStatus = status === "accepted" || status === "all" || status === "pending" ? status : "pending";
const safeLimit = Number.isInteger(limit) ? Math.min(50, Math.max(1, limit)) : 5;
const result = await client.listInsights(safeStatus, safeLimit);

Copilot uses AI. Check for mistakes.
if (!result) {
return { content: [{ type: "text" as const, text: "Failed to fetch insights." }] };
}
const items = (result as any)?.insights ?? [];
if (!items.length) {
return { content: [{ type: "text" as const, text: "No insights found." }] };
}
const text = items
.map((insight: any, i: number) => {
const conf = typeof insight.confidence === "number" ? ` (${Math.round(insight.confidence * 100)}%)` : "";
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

typeof insight.confidence === "number" will also be true for NaN/Infinity, which can produce user-visible NaN%/Infinity% in output. Use a finite-number check (e.g., Number.isFinite) before formatting the percentage.

Suggested change
const conf = typeof insight.confidence === "number" ? ` (${Math.round(insight.confidence * 100)}%)` : "";
const conf = Number.isFinite(insight.confidence) ? ` (${Math.round(insight.confidence * 100)}%)` : "";

Copilot uses AI. Check for mistakes.
const type = insight.insight_type ? ` [${insight.insight_type}]` : "";
const statusTag = insight.status ? ` [${insight.status}]` : "";
return `${i + 1}. ${insight.insight ?? insight.content ?? JSON.stringify(insight)}${conf}${type}${statusTag}`;
Comment on lines +1730 to +1735
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

Falling back to JSON.stringify(insight) can emit very large blobs and may unintentionally surface fields that shouldn't be shown to the user. Prefer selecting a small, known-safe subset of fields (and truncating long text) for the fallback case to keep responses bounded and predictable.

Suggested change
const text = items
.map((insight: any, i: number) => {
const conf = typeof insight.confidence === "number" ? ` (${Math.round(insight.confidence * 100)}%)` : "";
const type = insight.insight_type ? ` [${insight.insight_type}]` : "";
const statusTag = insight.status ? ` [${insight.status}]` : "";
return `${i + 1}. ${insight.insight ?? insight.content ?? JSON.stringify(insight)}${conf}${type}${statusTag}`;
const truncateText = (value: unknown, max = 200): string | undefined => {
if (typeof value !== "string") return undefined;
const normalized = value.trim();
if (!normalized) return undefined;
return normalized.length > max ? `${normalized.slice(0, max - 1)}…` : normalized;
};
const formatInsightFallback = (insight: any): string => {
const parts = [
truncateText(insight?.title, 80),
truncateText(insight?.summary, 160),
typeof insight?.id === "string" && insight.id.trim() ? `id=${insight.id.trim()}` : undefined,
typeof insight?.insight_type === "string" && insight.insight_type.trim()
? `type=${insight.insight_type.trim()}`
: undefined,
typeof insight?.status === "string" && insight.status.trim() ? `status=${insight.status.trim()}` : undefined,
typeof insight?.confidence === "number" ? `confidence=${Math.round(insight.confidence * 100)}%` : undefined,
].filter((part): part is string => Boolean(part));
return parts.length ? parts.join(" | ") : "Insight details unavailable";
};
const text = items
.map((insight: any, i: number) => {
const conf = typeof insight.confidence === "number" ? ` (${Math.round(insight.confidence * 100)}%)` : "";
const type = insight.insight_type ? ` [${insight.insight_type}]` : "";
const statusTag = insight.status ? ` [${insight.status}]` : "";
const displayText =
truncateText(insight.insight, 280) ??
truncateText(insight.content, 280) ??
formatInsightFallback(insight);
return `${i + 1}. ${displayText}${conf}${type}${statusTag}`;

Copilot uses AI. Check for mistakes.
})
.join("\n");
return { content: [{ type: "text" as const, text: `Found ${items.length} insight(s):\n\n${text}` }] };
} catch (err) {
return { content: [{ type: "text" as const, text: `List insights failed: ${String(err)}` }] };
}
},
},
{ name: "cortex_insights" },
);
Comment on lines +1710 to +1745
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: cortex_insights tool does not expose owner_id parameter unlike peer tools

Most similar tools (e.g., cortex_list_commitments, cortex_list_open_loops, cortex_list_contradictions) expose an optional owner_id parameter allowing callers to override the default owner namespace. The cortex_insights tool and its underlying listInsights method always use this.ownerId with no override. This may be intentional (insights are always scoped to the configured owner) but it's a deviation from the pattern of the sibling tools. If multi-owner insight queries are ever needed, this would require a change.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


api.registerTool(
{
name: "cortex_add_open_loop",
Expand Down
Loading