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
12 changes: 12 additions & 0 deletions docs/contexto.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,15 @@ To ensure the agent always has access to your latest thoughts, Contexto implemen

- **Live Tracking:** It listens to the configured `knowledgeFolder` (your Obsidian vault).
- **Auto-Embedding:** Any changes, additions, or modifications to your markdown files trigger a debounced update. The system automatically recompiles and embeds the new information into the QMD index, keeping the knowledge base perpetually fresh.

## 4. Tuning Retrieval

Three retrieval knobs are exposed through the plugin config — defaults match what's hardwired into `engine/base.ts` and what `Mindmap.search` uses internally:

| Setting | Default | Effect |
| --- | --- | --- |
| `maxResults` | `7` | Upper bound on items returned per `assemble()` call before dedup against the already-injected set. |
| `minScore` | `0.45` | Similarity floor. Items scoring below this are dropped before formatting. |
| `filter` | `{}` | Metadata-equality filter merged into the backend search. The engine pins `source: 'summary'`; anything you set here is spread on top, so passing `{ source: 'episode' }` switches the kind of items returned. |

See the package [README's Configuration table](../packages/contexto/README.md#configuration) for the full property list.
5 changes: 5 additions & 0 deletions packages/contexto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ For the deeper technical reasoning:
| --- | --- | --- | --- |
| `apiKey` | string | Yes (remote) | Your Contexto API key |
| `mode` | string | No | `remote` (default) or `local` |
| `contextEnabled` | boolean | No | Set to `false` to disable context retrieval and injection (default: `true`) |
| `maxContextChars` | number | No | Cap on the assembled context block, in characters (default: `2000`) |
| `maxResults` | number | No | Max number of memory items retrieved per `assemble()` call (default: `7`) |
| `minScore` | number | No | Minimum similarity score a candidate item must exceed (default: `0.45`) |
| `filter` | object | No | Metadata-equality filter merged into backend search; engine pins `source: 'summary'` and your filter is spread on top |

### Remote mode (default)

Expand Down
14 changes: 14 additions & 0 deletions packages/contexto/openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@
"type": "number",
"description": "Maximum characters of context to inject (default: 2000)"
},
"maxResults": {
"type": "number",
"default": 7,
"description": "Maximum number of memory items to retrieve per assemble() call (default: 7)"
},
"minScore": {
"type": "number",
"default": 0.45,
"description": "Minimum similarity score for a memory item to be considered relevant (default: 0.45)"
},
"filter": {
"type": "object",
"description": "Optional metadata-equality filter merged into the backend search. The engine pins source='summary'; anything set here is spread on top."
},
"mode": {
"type": "string",
"default": "remote",
Expand Down
18 changes: 12 additions & 6 deletions packages/contexto/src/engine/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,24 @@ export abstract class AbstractContextEngine implements ContextEngine {
this.logger.info(`[contexto] Fetching context for query: "${query.slice(0, 100)}"`);
const filter = { source: 'summary', ...this.config.filter };
const minScore = this.config.minScore ?? DEFAULT_MIN_SCORE;
const result = await this.backend.search(query, DEFAULT_MAX_RESULTS, filter, minScore);
const maxResults = this.config.maxResults ?? DEFAULT_MAX_RESULTS;
const result = await this.backend.search(query, maxResults, filter, minScore);

if (!result?.items?.length) {
return { messages, estimatedTokens: 0 };
}

const itemSummary = result.items.map((r: any, i: number) => `[${i}] ${r.item?.content?.length ?? 0} chars`).join(', ');
const itemSummary = result.items.map((r, i) => `[${i}] ${r.item?.content?.length ?? 0} chars`).join(', ');
this.logger.info(`[contexto] Mindmap returned ${result.items.length} items (${itemSummary}), paths: ${JSON.stringify(result.paths)}`);

// Deduplicate: filter out items already injected in this session
const filtered = result.items.filter((r: any) => {
const id = (r.item ?? r).id;
// Deduplicate: filter out items already injected in this session.
// The dedup key is the inner item's id; older defensive code used
// ``(r.item ?? r).id`` to also support a raw-item shape, but both
// backends (local + remote) wrap items in ``{item, score, ...}``
// and ``MindmapItem.id`` is required — so the inner access is
// always defined.
const filtered = result.items.filter((r) => {
const id = r.item?.id;
return !id || !this.state.injectedItemIds.has(id);
});

Expand All @@ -112,7 +118,7 @@ export abstract class AbstractContextEngine implements ContextEngine {

// Record injected item IDs
for (const r of filtered) {
const id = (r.item ?? r).id;
const id = r.item?.id;
if (id) this.state.injectedItemIds.add(id);
}

Expand Down
15 changes: 14 additions & 1 deletion packages/contexto/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import { LocalBackend } from './local/index.js';
import type { ResolvedCredentials } from './local/index.js';
import { createContextEngine } from './engine/index.js';

export type { ContextoBackend, SearchResult, WebhookPayload, Logger } from './types.js';
export type {
ContextoBackend,
MindmapItem,
ScoredMindmapItem,
SearchResult,
WebhookPayload,
Logger,
} from './types.js';
export { RemoteBackend } from './client.js';
export { LocalBackend } from './local/index.js';
export type { LocalBackendConfig, ResolvedCredentials, EpisodeSummary } from './local/index.js';
Expand All @@ -21,6 +28,9 @@ export default {
apiKey: { type: 'string' },
contextEnabled: { type: 'boolean', default: true },
maxContextChars: { type: 'number' },
maxResults: { type: 'number', default: 7 },
minScore: { type: 'number', default: 0.45 },
filter: { type: 'object' },
compactThreshold: { type: 'number', default: 0.50 },
compactionStrategy: { type: 'string', default: 'default' },
mode: { type: 'string', default: 'remote' },
Expand All @@ -36,6 +46,9 @@ export default {
apiKey: api.pluginConfig?.apiKey,
contextEnabled: api.pluginConfig?.contextEnabled ?? true,
maxContextChars: api.pluginConfig?.maxContextChars,
maxResults: api.pluginConfig?.maxResults,
minScore: api.pluginConfig?.minScore,
filter: api.pluginConfig?.filter,
mode: backendMode as 'remote' | 'local',
};

Expand Down
33 changes: 32 additions & 1 deletion packages/contexto/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ export interface BaseConfig {
contextEnabled?: boolean;

maxContextChars?: number;
/** Max number of memory items to retrieve per assemble() call. Default: 7. */
maxResults?: number;
/** Minimum similarity score for an item to be considered relevant. Default: 0.45. */
minScore?: number;
/**
* Optional metadata-equality filter merged into the backend search.
* The engine always pins ``source: 'summary'`` — anything you set here is
* spread on top, so passing ``{ source: 'episode' }`` switches the kind
* of items returned. Pass ``{}`` to keep the default.
*/
filter?: Record<string, unknown>;
mode?: 'remote' | 'local';
}
Expand Down Expand Up @@ -31,9 +40,31 @@ export interface WebhookPayload {
data?: Record<string, unknown>;
}

/**
* One memory item carried in a mindmap search result. Matches
* ``@ekai/mindmap``'s ``ConversationItem`` shape (the storage record),
* minus the embedding vector which the search layer never echoes back
* to callers. Re-exposed here so backend implementers (#116) don't
* have to import from a different package to learn the field names.
*/
export interface MindmapItem {
id: string;
role: string;
content: string;
timestamp?: string;
metadata?: Record<string, unknown>;
}

/** A search hit: the item + its similarity score + an estimated token cost. */
export interface ScoredMindmapItem {
item: MindmapItem;
score: number;
estimatedTokens: number;
}

/** Shape returned by the mindmap search endpoint (ScoredQueryResult). */
export interface SearchResult {
items: any[];
items: ScoredMindmapItem[];
paths?: string[][];
}

Expand Down
Loading