feat: sync open-source context and capsule removal
- remove legacy contexts surfaces from the app, MCP bridge, standalone server, and docs - keep getContext and retrieveQueryContext while aligning the simplified graph-only contract - harden dev startup migration behavior and disable the accidental chat surface in open source Generated with Codex
This commit is contained in:
@@ -80,12 +80,10 @@ Do not create contradictory instruction files. Prefer one short reinforcement li
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `getContext` | Get graph overview - stats, contexts, recent activity |
|
||||
| `getContext` | Get graph overview - stats, hub nodes, recent activity |
|
||||
| `retrieveQueryContext` | Pull relevant graph context for a broader current-turn task |
|
||||
| `createNode` | Create a new node |
|
||||
| `writeContext` | Save one confirmed durable context node after explicit user approval |
|
||||
| `queryNodes` | Search nodes by keyword |
|
||||
| `queryContexts` | List or inspect contexts |
|
||||
| `getNodesById` | Load nodes by ID |
|
||||
| `updateNode` | Update an existing node |
|
||||
| `createEdge` | Create a confirmed connection between nodes |
|
||||
@@ -121,19 +119,11 @@ Rules:
|
||||
- use `captured_by = "human"` for direct user creation and user-requested agent capture
|
||||
- reserve `captured_by = "agent"` for autonomous/background creation only
|
||||
|
||||
## Context Rule
|
||||
|
||||
- creating a node never requires context
|
||||
- normal node lookup and update flows should omit context unless the user explicitly asks for it
|
||||
- if context is intentionally provided, prefer `context_name`
|
||||
- numeric `context_id` is treated as an internal implementation detail rather than a normal agent-facing field
|
||||
|
||||
## Writeback Rule
|
||||
|
||||
- do not ask to save every moderately useful point from the conversation
|
||||
- only suggest a save when the context is unusually durable and valuable
|
||||
- keep the ask terse and concrete, for example: `Add "X" as a node?`
|
||||
- never call `writeContext` unless the user has explicitly said yes
|
||||
|
||||
## Edge Rule
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ const packageJson = require('./package.json');
|
||||
const { initDatabase, getDatabasePath, closeDatabase, getDb, query } = require('./services/sqlite-client');
|
||||
const nodeService = require('./services/nodeService');
|
||||
const edgeService = require('./services/edgeService');
|
||||
const contextService = require('./services/contextService');
|
||||
const skillService = require('./services/skillService');
|
||||
const retrievalService = require('./services/retrievalService');
|
||||
const { directNodeLookup } = require('./services/directNodeLookupService');
|
||||
@@ -68,17 +67,10 @@ function buildInstructions() {
|
||||
5. For simple tasks, tool descriptions have everything you need.
|
||||
6. For complex tasks, call readSkill("db-operations").
|
||||
|
||||
## Context field rule
|
||||
Context is optional on writes.
|
||||
Do not include any context field unless the user explicitly wants one.
|
||||
If context is intentionally provided, prefer \`context_name\`. Treat numeric \`context_id\` as an internal implementation detail.
|
||||
Omitting context is the normal default and does not block create or update operations.
|
||||
|
||||
## Knowledge capture
|
||||
Only suggest saving context when it seems unusually durable and valuable.
|
||||
Only suggest saving durable knowledge when it seems unusually durable and valuable.
|
||||
Keep the ask brief: Add "X" as a node?
|
||||
Do not pester. Do not keep re-asking if the user says no, ignores it, or moves on.
|
||||
Never write via writeContext unless the user has explicitly confirmed yes.
|
||||
Do not create edges autonomously. Surface likely edge candidates briefly, then call edge-write tools only after the user explicitly confirms.
|
||||
Always search or retrieve before creating to avoid duplicates.
|
||||
|
||||
@@ -96,7 +88,6 @@ const addNodeInputSchema = {
|
||||
source: z.string().max(50000).optional().describe('Canonical source content for embedding'),
|
||||
link: z.string().url().optional().describe('Source URL'),
|
||||
description: z.string().optional().describe('Strongly recommended. Write the description as natural prose, not labels or a checklist. It should make clear what the artifact is and any surrounding context available. RA-H will accept whatever description is provided and will not block the write.'),
|
||||
context_name: z.string().optional().describe('Optional primary context name. Use only when the user explicitly wants this node assigned to a known context.'),
|
||||
metadata: z.record(z.any()).optional().describe('Optional metadata. Prefer canonical keys: type, state, captured_method, captured_by, source_metadata.'),
|
||||
chunk: z.string().max(50000).optional().describe('Legacy alias for source text')
|
||||
};
|
||||
@@ -104,7 +95,6 @@ const addNodeInputSchema = {
|
||||
const searchNodesInputSchema = {
|
||||
query: z.string().min(1).max(400).describe('Search query'),
|
||||
limit: z.number().min(1).max(50).optional().describe('Max results (default 10)'),
|
||||
context_name: z.string().optional().describe('Optional primary context name filter. Use only when the user explicitly wants a context-specific lookup.'),
|
||||
created_after: z.string().optional().describe('ISO date (YYYY-MM-DD). Only return nodes created on or after this date.'),
|
||||
created_before: z.string().optional().describe('ISO date (YYYY-MM-DD). Only return nodes created before this date.'),
|
||||
event_after: z.string().optional().describe('ISO date (YYYY-MM-DD). Only return nodes with event_date on or after this date.'),
|
||||
@@ -114,19 +104,9 @@ const searchNodesInputSchema = {
|
||||
const retrieveQueryContextInputSchema = {
|
||||
query: z.string().min(1).max(800).describe('The raw user query for this turn'),
|
||||
focused_node_id: z.number().int().positive().nullable().optional().describe('Optional currently focused node ID'),
|
||||
active_context_id: z.number().int().positive().nullable().optional().describe('Optional active context ID as a soft hint'),
|
||||
limit: z.number().min(1).max(12).optional().describe('Maximum number of nodes to return')
|
||||
};
|
||||
|
||||
const writeContextInputSchema = {
|
||||
title: z.string().min(1).max(160).describe('Clear proposed node title'),
|
||||
description: z.string().min(1).max(500).describe('Natural description of what this context is and why it matters'),
|
||||
source: z.string().max(50000).optional().describe('Optional source or verbatim user wording to preserve'),
|
||||
context_name: z.string().optional().describe('Optional primary context name. Use only when the user explicitly wants this saved under a known context.'),
|
||||
metadata: z.record(z.any()).optional().describe('Optional metadata patch'),
|
||||
confirmed_by_user: z.boolean().describe('Must be true before the write is allowed')
|
||||
};
|
||||
|
||||
const getNodesInputSchema = {
|
||||
nodeIds: z.array(z.number().int().positive()).min(1).max(10).describe('Node IDs to load')
|
||||
};
|
||||
@@ -139,8 +119,6 @@ const updateNodeInputSchema = {
|
||||
content: z.string().optional().describe('Legacy alias for source'),
|
||||
source: z.string().optional().describe('Canonical source content for embedding'),
|
||||
link: z.string().optional().describe('New link'),
|
||||
context_name: z.string().optional().describe('Optional primary context name. Use only when the user explicitly wants to assign this node to a known context.'),
|
||||
clear_context: z.boolean().optional().describe('Set true only when the user explicitly wants to remove the node context.'),
|
||||
metadata: z.record(z.any()).optional().describe('Metadata patch. It now merges with existing metadata. Prefer canonical keys: type, state, captured_method, captured_by, source_metadata.')
|
||||
}).describe('Fields to update')
|
||||
};
|
||||
@@ -163,14 +141,6 @@ const queryEdgesInputSchema = {
|
||||
limit: z.number().min(1).max(50).optional().describe('Max edges (default 25)')
|
||||
};
|
||||
|
||||
const queryContextsInputSchema = {
|
||||
contextId: z.number().int().positive().optional().describe('Exact context ID lookup'),
|
||||
name: z.string().optional().describe('Exact context name lookup'),
|
||||
search: z.string().optional().describe('Case-insensitive search across context names and descriptions'),
|
||||
limit: z.number().min(1).max(100).optional().describe('Maximum number of contexts to return'),
|
||||
includeNodes: z.boolean().optional().describe('Include nodes for an exact single-context lookup')
|
||||
};
|
||||
|
||||
const readSkillInputSchema = {
|
||||
name: z.string().min(1).describe('Skill name (e.g. "db-operations", "onboarding", "persona")')
|
||||
};
|
||||
@@ -276,7 +246,7 @@ async function main() {
|
||||
'getContext',
|
||||
{
|
||||
title: 'Get RA-H context',
|
||||
description: 'Get knowledge graph overview: stats, contexts, hub nodes (secondary diagnostics), recent activity, and available skills. Use this for orientation only, not as the default retrieval path for substantive requests. For deeper operating policy, follow up with readSkill("db-operations").',
|
||||
description: 'Get knowledge graph overview: stats, hub nodes, recent activity, and available skills. Use this for orientation only, not as the default retrieval path for substantive requests. For deeper operating policy, follow up with readSkill("db-operations").',
|
||||
inputSchema: {}
|
||||
},
|
||||
async () => {
|
||||
@@ -288,16 +258,16 @@ async function main() {
|
||||
// First-run welcome message
|
||||
if (context.stats.nodeCount === 0) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Empty knowledge graph. This is a fresh start. Ask what matters right now and help create the first useful node. Contexts are optional and can wait until one is obviously helpful.' }],
|
||||
content: [{ type: 'text', text: 'Empty knowledge graph. This is a fresh start. Ask what matters right now and help create the first useful node.' }],
|
||||
structuredContent: {
|
||||
...context,
|
||||
welcome: true,
|
||||
suggestion: 'Ask what matters right now, create the first useful node, and leave contexts empty unless one is an obvious fit.'
|
||||
suggestion: 'Ask what matters right now and create the first useful node.'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const summary = `Graph: ${context.stats.contextCount || 0} contexts, ${context.stats.nodeCount} nodes, ${context.stats.edgeCount} edges, ${skills.length} skills.`;
|
||||
const summary = `Graph: ${context.stats.nodeCount} nodes, ${context.stats.edgeCount} edges, ${skills.length} skills.`;
|
||||
return {
|
||||
content: [{ type: 'text', text: summary }],
|
||||
structuredContent: context
|
||||
@@ -314,11 +284,10 @@ async function main() {
|
||||
description: 'Given the raw user query plus optional focused node state, retrieve the most relevant graph context for the current turn. It starts with direct graph search and broadens only if useful. Use this when graph context could help answer or complete a broader task. For explicit node lookup, use queryNodes.',
|
||||
inputSchema: retrieveQueryContextInputSchema
|
||||
},
|
||||
async ({ query: rawQuery, focused_node_id, active_context_id, limit = 6 }) => {
|
||||
async ({ query: rawQuery, focused_node_id, limit = 6 }) => {
|
||||
const result = retrievalService.retrieveQueryContext({
|
||||
query: rawQuery,
|
||||
focused_node_id: focused_node_id ?? null,
|
||||
active_context_id: active_context_id ?? null,
|
||||
limit,
|
||||
});
|
||||
|
||||
@@ -337,26 +306,18 @@ async function main() {
|
||||
'createNode',
|
||||
{
|
||||
title: 'Add RA-H node',
|
||||
description: 'Create a new node. Always search first (queryNodes) to avoid duplicates. If the user explicitly asked to save or import something and the target artifact is clear, write after duplicate/update checks. If you are only suggesting a save, propose the node first and wait for confirmation. Leave context blank by default. If the user explicitly wants context, use `context_name` rather than a numeric ID. Title: max 160 chars, clear and descriptive. Description is strongly recommended and should explicitly describe what the thing is and any surrounding context available, but the write will never be blocked over description quality. Use "link" ONLY for external content (URL, video, article) — omit for synthesis/ideas derived from existing nodes. "source" = verbatim or canonical content stored on the node. The RA-H app owns chunking and embedding from source. Legacy "content" and "chunk" are mapped to source for compatibility.',
|
||||
description: 'Create a new node. Always search first (queryNodes) to avoid duplicates. If the user explicitly asked to save or import something and the target artifact is clear, write after duplicate/update checks. If you are only suggesting a save, propose the node first and wait for confirmation. Title: max 160 chars, clear and descriptive. Description is strongly recommended and should explicitly describe what the thing is and any surrounding context available, but the write will never be blocked over description quality. Use "link" ONLY for external content (URL, video, article) — omit for synthesis/ideas derived from existing nodes. "source" = verbatim or canonical content stored on the node. The RA-H app owns chunking and embedding from source. Legacy "content" and "chunk" are mapped to source for compatibility.',
|
||||
inputSchema: addNodeInputSchema
|
||||
},
|
||||
async ({ title, content, source, link, description, context_name, metadata, chunk }) => {
|
||||
async ({ title, content, source, link, description, metadata, chunk }) => {
|
||||
const sourceText = source?.trim() || content?.trim() || chunk?.trim();
|
||||
const normalizedDescription = typeof description === 'string' ? description.trim() : description;
|
||||
|
||||
let resolvedContextId;
|
||||
try {
|
||||
resolvedContextId = contextService.resolveContextId({ context_name });
|
||||
} catch (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
const node = nodeService.createNode({
|
||||
title: title.trim(),
|
||||
source: sourceText,
|
||||
link: link?.trim(),
|
||||
description: normalizedDescription,
|
||||
context_id: resolvedContextId,
|
||||
metadata: metadata || {}
|
||||
});
|
||||
|
||||
@@ -377,15 +338,14 @@ async function main() {
|
||||
'queryNodes',
|
||||
{
|
||||
title: 'Search RA-H nodes',
|
||||
description: 'Search nodes by keyword across title, description, and source fields using the same safe direct-lookup behavior as the app. Use this for direct node lookup or duplicate checks. Leave context blank by default. If the user explicitly wants a context-specific lookup, use context_name rather than a numeric ID. For full current-turn grounding of a substantive query, prefer retrieveQueryContext. NOT for searching source documents (transcripts, articles) — use searchContentEmbeddings for that.',
|
||||
description: 'Search nodes by keyword across title, description, and source fields using the same safe direct-lookup behavior as the app. Use this for direct node lookup or duplicate checks. For full current-turn grounding of a substantive query, prefer retrieveQueryContext. NOT for searching source documents (transcripts, articles) — use searchContentEmbeddings for that.',
|
||||
inputSchema: searchNodesInputSchema
|
||||
},
|
||||
async ({ query: searchQuery, limit = 10, context_name, created_after, created_before, event_after, event_before }) => {
|
||||
async ({ query: searchQuery, limit = 10, created_after, created_before, event_after, event_before }) => {
|
||||
const safeLimit = Math.min(Math.max(limit, 1), 50);
|
||||
const result = directNodeLookup({
|
||||
search: searchQuery.trim(),
|
||||
limit: safeLimit,
|
||||
context_name,
|
||||
createdAfter: created_after,
|
||||
createdBefore: created_before,
|
||||
eventAfter: event_after,
|
||||
@@ -416,50 +376,6 @@ async function main() {
|
||||
}
|
||||
);
|
||||
|
||||
registerToolWithAliases(
|
||||
'writeContext',
|
||||
{
|
||||
title: 'Write RA-H context node',
|
||||
description: 'Write one atomic durable context node to the graph only after the user has explicitly approved the save. Use this for agent-suggested capture after you already proposed the node briefly and got a clear yes. Prefer ordinary create/update flows for explicit user-directed capture.',
|
||||
inputSchema: writeContextInputSchema
|
||||
},
|
||||
async ({ title, description, source, context_name, metadata, confirmed_by_user }) => {
|
||||
if (!confirmed_by_user) {
|
||||
throw new Error('writeContext requires explicit user confirmation before writing to the graph.');
|
||||
}
|
||||
|
||||
let resolvedContextId;
|
||||
try {
|
||||
resolvedContextId = contextService.resolveContextId({ context_name });
|
||||
} catch (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
const node = nodeService.createNode({
|
||||
title: title.trim(),
|
||||
description: description.trim(),
|
||||
source: source?.trim(),
|
||||
context_id: resolvedContextId,
|
||||
metadata: {
|
||||
captured_by: 'human',
|
||||
captured_method: 'write_context',
|
||||
...(metadata || {})
|
||||
}
|
||||
});
|
||||
|
||||
const summary = `Saved context as node #${node.id}: ${node.title}`;
|
||||
return {
|
||||
content: [{ type: 'text', text: summary }],
|
||||
structuredContent: {
|
||||
success: true,
|
||||
nodeId: node.id,
|
||||
title: node.title,
|
||||
message: summary
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
registerToolWithAliases(
|
||||
'getNodesById',
|
||||
{
|
||||
@@ -512,7 +428,7 @@ async function main() {
|
||||
'updateNode',
|
||||
{
|
||||
title: 'Update RA-H node',
|
||||
description: 'Update an existing node when it is clearly the same artifact and a net-new node would be redundant. Explicit user-directed updates can proceed once the target node is clear. Context is preserved by default. If the user explicitly wants to change context, use `context_name`. Use `clear_context` only when the user explicitly wants the context removed. Description updates should explicitly state what this thing is and any surrounding context available, but the write will never be blocked over description quality. Source content lives in "source". The RA-H app owns chunking and embedding from source. Legacy "content" is mapped to source for compatibility. Title, description, and link are overwritten. Call getNodesById first to verify current state before updating.',
|
||||
description: 'Update an existing node when it is clearly the same artifact and a net-new node would be redundant. Explicit user-directed updates can proceed once the target node is clear. Description updates should explicitly state what this thing is and any surrounding context available, but the write will never be blocked over description quality. Source content lives in "source". The RA-H app owns chunking and embedding from source. Legacy "content" is mapped to source for compatibility. Title, description, and link are overwritten. Call getNodesById first to verify current state before updating.',
|
||||
inputSchema: updateNodeInputSchema
|
||||
},
|
||||
async ({ id, updates }) => {
|
||||
@@ -537,19 +453,6 @@ async function main() {
|
||||
: mappedUpdates.description;
|
||||
}
|
||||
|
||||
if (mappedUpdates.context_name && mappedUpdates.clear_context) {
|
||||
throw new Error('context_name cannot be combined with clear_context: true.');
|
||||
}
|
||||
|
||||
if (mappedUpdates.context_name || mappedUpdates.clear_context || Object.prototype.hasOwnProperty.call(mappedUpdates, 'context_id')) {
|
||||
mappedUpdates.context_id = contextService.resolveContextId({
|
||||
context_id: mappedUpdates.clear_context ? null : mappedUpdates.context_id,
|
||||
context_name: mappedUpdates.context_name,
|
||||
});
|
||||
}
|
||||
delete mappedUpdates.context_name;
|
||||
delete mappedUpdates.clear_context;
|
||||
|
||||
const node = nodeService.updateNode(id, mappedUpdates);
|
||||
const message = `Updated node #${id}`;
|
||||
|
||||
@@ -652,73 +555,6 @@ async function main() {
|
||||
|
||||
// ========== DIMENSION TOOLS ==========
|
||||
|
||||
registerToolWithAliases(
|
||||
'queryContexts',
|
||||
{
|
||||
title: 'List RA-H contexts',
|
||||
description: 'List or inspect optional contexts. Use this only when a context is already obviously relevant or the user asks for it.',
|
||||
inputSchema: queryContextsInputSchema
|
||||
},
|
||||
async ({ contextId, name, search, limit = 50, includeNodes = false }) => {
|
||||
const normalizedName = typeof name === 'string' ? name.trim() : '';
|
||||
const normalizedSearch = typeof search === 'string' ? search.trim().toLowerCase() : '';
|
||||
|
||||
let contexts = [];
|
||||
|
||||
if (contextId) {
|
||||
const context = contextService.getContextById(contextId);
|
||||
contexts = context ? [context] : [];
|
||||
} else {
|
||||
contexts = contextService.listContexts();
|
||||
}
|
||||
|
||||
if (normalizedName) {
|
||||
contexts = contexts.filter((context) => context.name.toLowerCase() === normalizedName.toLowerCase());
|
||||
}
|
||||
|
||||
if (normalizedSearch) {
|
||||
contexts = contexts.filter((context) =>
|
||||
context.name.toLowerCase().includes(normalizedSearch) ||
|
||||
(context.description || '').toLowerCase().includes(normalizedSearch)
|
||||
);
|
||||
}
|
||||
|
||||
contexts = contexts.slice(0, Math.min(Math.max(limit, 1), 100));
|
||||
|
||||
const includeContextNodes = includeNodes && contexts.length === 1 && (contextId || normalizedName);
|
||||
const structuredContexts = contexts.map((context) => {
|
||||
if (!includeContextNodes) {
|
||||
return context;
|
||||
}
|
||||
|
||||
const nodes = nodeService.getNodes({ contextId: context.id, limit: 500 });
|
||||
return {
|
||||
...context,
|
||||
nodes: nodes.map((node) => ({
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
description: node.description ?? null,
|
||||
link: node.link ?? null,
|
||||
context_id: node.context_id ?? null,
|
||||
updated_at: node.updated_at,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const summary = structuredContexts.length === 0
|
||||
? 'No contexts found.'
|
||||
: `Found ${structuredContexts.length} context(s).`;
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: summary }],
|
||||
structuredContent: {
|
||||
count: structuredContexts.length,
|
||||
contexts: structuredContexts,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ========== SKILL TOOLS ==========
|
||||
|
||||
registerToolWithAliases(
|
||||
@@ -934,7 +770,7 @@ async function main() {
|
||||
'sqliteQuery',
|
||||
{
|
||||
title: 'Execute read-only SQL',
|
||||
description: 'Execute read-only SQL queries against the knowledge graph database. Tables: nodes, contexts, edges, chunks, and migration snapshots. Use PRAGMA table_info(tablename) for schema. Only SELECT/WITH/PRAGMA allowed. Use when structured tools are insufficient — e.g., complex JOINs, aggregations, or custom filtering. Read readSkill("schema") for table definitions and query patterns.',
|
||||
description: 'Execute read-only SQL queries against the knowledge graph database. Tables: nodes, edges, chunks, chats, voice_usage, logs, and migration snapshots. Use PRAGMA table_info(tablename) for schema. Only SELECT/WITH/PRAGMA allowed. Use when structured tools are insufficient — e.g., complex JOINs, aggregations, or custom filtering. Read readSkill("schema") for table definitions and query patterns.',
|
||||
inputSchema: sqliteQueryInputSchema
|
||||
},
|
||||
async ({ sql: userSql, format = 'json' }) => {
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { getDb } = require('./sqlite-client');
|
||||
const MAX_CONTEXTS_PER_ACCOUNT = 10;
|
||||
|
||||
function mapContext(row) {
|
||||
if (!row) return null;
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description ?? null,
|
||||
icon: row.icon ?? null,
|
||||
count: Number(row.count ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
function listContexts() {
|
||||
const db = getDb();
|
||||
const rows = db.prepare(`
|
||||
SELECT c.id, c.name, c.description, c.icon, COUNT(n.id) AS count
|
||||
FROM contexts c
|
||||
LEFT JOIN nodes n ON n.context_id = c.id
|
||||
GROUP BY c.id
|
||||
ORDER BY c.name COLLATE NOCASE ASC
|
||||
`).all();
|
||||
return rows.map(mapContext);
|
||||
}
|
||||
|
||||
function getContextById(id) {
|
||||
const db = getDb();
|
||||
const row = db.prepare(`
|
||||
SELECT c.id, c.name, c.description, c.icon, COUNT(n.id) AS count
|
||||
FROM contexts c
|
||||
LEFT JOIN nodes n ON n.context_id = c.id
|
||||
WHERE c.id = ?
|
||||
GROUP BY c.id
|
||||
`).get(id);
|
||||
return mapContext(row);
|
||||
}
|
||||
|
||||
function getContextByName(name) {
|
||||
const trimmed = typeof name === 'string' ? name.trim() : '';
|
||||
if (!trimmed) return null;
|
||||
const db = getDb();
|
||||
const row = db.prepare(`
|
||||
SELECT c.id, c.name, c.description, c.icon, COUNT(n.id) AS count
|
||||
FROM contexts c
|
||||
LEFT JOIN nodes n ON n.context_id = c.id
|
||||
WHERE lower(c.name) = lower(?)
|
||||
GROUP BY c.id
|
||||
`).get(trimmed);
|
||||
return mapContext(row);
|
||||
}
|
||||
|
||||
function createContext({ name, description = null, icon = null }) {
|
||||
const db = getDb();
|
||||
const trimmedName = typeof name === 'string' ? name.trim() : '';
|
||||
if (!trimmedName) {
|
||||
throw new Error('Context name is required.');
|
||||
}
|
||||
const existingCount = Number(db.prepare('SELECT COUNT(*) AS count FROM contexts').get()?.count ?? 0);
|
||||
if (existingCount >= MAX_CONTEXTS_PER_ACCOUNT) {
|
||||
throw new Error(`Context limit reached. Maximum ${MAX_CONTEXTS_PER_ACCOUNT} contexts are allowed per account.`);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const info = db.prepare(`
|
||||
INSERT INTO contexts (name, description, icon, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(trimmedName, description ?? null, icon ?? null, now, now);
|
||||
|
||||
return getContextById(Number(info.lastInsertRowid));
|
||||
}
|
||||
|
||||
function updateContext({ id, name, description, icon }) {
|
||||
const db = getDb();
|
||||
const existing = getContextById(id);
|
||||
if (!existing) {
|
||||
throw new Error(`Context ${id} not found.`);
|
||||
}
|
||||
|
||||
const nextName = typeof name === 'string' && name.trim() ? name.trim() : existing.name;
|
||||
const nextDescription = description === undefined ? existing.description : description;
|
||||
const nextIcon = icon === undefined ? existing.icon : icon;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
db.prepare(`
|
||||
UPDATE contexts
|
||||
SET name = ?, description = ?, icon = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(nextName, nextDescription ?? null, nextIcon ?? null, now, id);
|
||||
|
||||
return getContextById(id);
|
||||
}
|
||||
|
||||
function resolveContextId(input = {}) {
|
||||
const hasContextId = Object.prototype.hasOwnProperty.call(input, 'context_id');
|
||||
const hasContextName = typeof input.context_name === 'string' && input.context_name.trim().length > 0;
|
||||
|
||||
if (!hasContextId && !hasContextName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (hasContextId && input.context_id === null) {
|
||||
if (hasContextName) {
|
||||
throw new Error('context_name cannot be combined with context_id: null.');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let resolvedById = null;
|
||||
if (hasContextId) {
|
||||
resolvedById = getContextById(input.context_id);
|
||||
if (!resolvedById) {
|
||||
throw new Error(`Context ${input.context_id} not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasContextName) {
|
||||
return resolvedById ? resolvedById.id : undefined;
|
||||
}
|
||||
|
||||
const byName = getContextByName(input.context_name);
|
||||
if (!byName) {
|
||||
throw new Error(`Context "${input.context_name}" not found.`);
|
||||
}
|
||||
if (resolvedById && resolvedById.id !== byName.id) {
|
||||
throw new Error('context_id and context_name refer to different contexts.');
|
||||
}
|
||||
return byName.id;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MAX_CONTEXTS_PER_ACCOUNT,
|
||||
listContexts,
|
||||
getContextById,
|
||||
getContextByName,
|
||||
createContext,
|
||||
updateContext,
|
||||
resolveContextId,
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const nodeService = require('./nodeService');
|
||||
const contextService = require('./contextService');
|
||||
|
||||
const SEARCH_STOP_WORDS = new Set([
|
||||
'a', 'about', 'added', 'already', 'an', 'and', 'are', 'as', 'at', 'be', 'by',
|
||||
@@ -109,41 +108,6 @@ function scoreNodeSearchMatch(node, query) {
|
||||
return score;
|
||||
}
|
||||
|
||||
function normalizeContextName(value) {
|
||||
if (typeof value !== 'string') return undefined;
|
||||
const normalized = value.trim().replace(/\s+/g, ' ');
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function resolveSearchContext({ context_name, contextId }) {
|
||||
const normalizedName = normalizeContextName(context_name);
|
||||
if (normalizedName) {
|
||||
const context = contextService.getContextByName(normalizedName);
|
||||
if (!context) {
|
||||
console.warn(`directNodeLookupService received unknown context_name "${normalizedName}"; ignoring context filter.`);
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
contextId: context.id,
|
||||
context_name: context.name,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof contextId === 'number') {
|
||||
const context = contextService.getContextById(contextId);
|
||||
if (!context) {
|
||||
console.warn(`directNodeLookupService received invalid legacy contextId ${contextId}; ignoring context filter.`);
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
contextId: context.id,
|
||||
context_name: context.name,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function hasStrongAnchorMatch(nodes, searchTerm) {
|
||||
if (!searchTerm || nodes.length === 0) return false;
|
||||
const highSignalTerms = getHighSignalSearchTerms(searchTerm);
|
||||
@@ -168,12 +132,9 @@ function directNodeLookup(input = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedContext = resolveSearchContext(input);
|
||||
|
||||
let safeNodes = nodeService.getNodes({
|
||||
search: searchTerm || undefined,
|
||||
limit,
|
||||
contextId: resolvedContext.contextId,
|
||||
createdAfter: input.createdAfter,
|
||||
createdBefore: input.createdBefore,
|
||||
eventAfter: input.eventAfter,
|
||||
@@ -181,7 +142,6 @@ function directNodeLookup(input = {}) {
|
||||
});
|
||||
|
||||
const hadExtraFilters = Boolean(
|
||||
resolvedContext.contextId !== undefined ||
|
||||
input.createdAfter ||
|
||||
input.createdBefore ||
|
||||
input.eventAfter ||
|
||||
@@ -209,7 +169,6 @@ function directNodeLookup(input = {}) {
|
||||
filtersApplied: {
|
||||
search: searchTerm || undefined,
|
||||
limit,
|
||||
context_name: resolvedContext.context_name,
|
||||
createdAfter: input.createdAfter,
|
||||
createdBefore: input.createdBefore,
|
||||
eventAfter: input.eventAfter,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { query, transaction, getDb } = require('./sqlite-client');
|
||||
const contextService = require('./contextService');
|
||||
|
||||
function parseMetadata(metadata) {
|
||||
if (!metadata) return {};
|
||||
@@ -54,8 +53,6 @@ function mapNodeRow(row) {
|
||||
return {
|
||||
...row,
|
||||
metadata: row.metadata ? (typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata) : null,
|
||||
context: row.context_json ? JSON.parse(row.context_json) : null,
|
||||
context_json: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,17 +60,16 @@ function mapNodeRow(row) {
|
||||
* Get nodes with optional filtering.
|
||||
*/
|
||||
function getNodes(filters = {}) {
|
||||
const { search, limit = 100, offset = 0, contextId } = filters;
|
||||
const { search, limit = 100, offset = 0 } = filters;
|
||||
|
||||
if (normalizeString(search)) {
|
||||
return searchNodes(filters);
|
||||
}
|
||||
|
||||
let sql = `
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.created_at, n.updated_at, n.context_id,
|
||||
CASE
|
||||
WHEN c.id IS NULL THEN NULL
|
||||
ELSE json_object('id', c.id, 'name', c.name, 'description', c.description, 'icon', c.icon)
|
||||
END as context_json
|
||||
n.created_at, n.updated_at
|
||||
FROM nodes n
|
||||
LEFT JOIN contexts c ON c.id = n.context_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
@@ -83,11 +79,6 @@ function getNodes(filters = {}) {
|
||||
sql += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.source LIKE ? COLLATE NOCASE)`;
|
||||
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
||||
}
|
||||
if (contextId !== undefined) {
|
||||
sql += ' AND n.context_id = ?';
|
||||
params.push(contextId);
|
||||
}
|
||||
|
||||
// Sort by search relevance or updated_at
|
||||
if (search) {
|
||||
sql += ` ORDER BY
|
||||
@@ -130,13 +121,8 @@ function searchNodes(filters = {}) {
|
||||
function getNodeById(id) {
|
||||
const sql = `
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.created_at, n.updated_at, n.context_id,
|
||||
CASE
|
||||
WHEN c.id IS NULL THEN NULL
|
||||
ELSE json_object('id', c.id, 'name', c.name, 'description', c.description, 'icon', c.icon)
|
||||
END as context_json
|
||||
n.created_at, n.updated_at
|
||||
FROM nodes n
|
||||
LEFT JOIN contexts c ON c.id = n.context_id
|
||||
WHERE n.id = ?
|
||||
`;
|
||||
|
||||
@@ -172,8 +158,7 @@ function createNode(nodeData) {
|
||||
source,
|
||||
link,
|
||||
event_date,
|
||||
metadata = {},
|
||||
context_id
|
||||
metadata = {}
|
||||
} = nodeData;
|
||||
|
||||
const title = sanitizeTitle(rawTitle);
|
||||
@@ -183,13 +168,12 @@ function createNode(nodeData) {
|
||||
const db = getDb();
|
||||
|
||||
const sourceToStore = source ?? ([title, description].filter(Boolean).join('\n\n').trim() || null);
|
||||
const effectiveContextId = context_id ?? null;
|
||||
const chunkStatus = getChunkStatusForSource(sourceToStore);
|
||||
|
||||
const nodeId = transaction(() => {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO nodes (title, description, source, link, event_date, metadata, chunk_status, context_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO nodes (title, description, source, link, event_date, metadata, chunk_status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
@@ -200,7 +184,6 @@ function createNode(nodeData) {
|
||||
event_date ?? null,
|
||||
JSON.stringify(canonicalMetadata),
|
||||
chunkStatus,
|
||||
effectiveContextId ?? null,
|
||||
now,
|
||||
now
|
||||
);
|
||||
@@ -259,10 +242,6 @@ function updateNode(id, updates, options = {}) {
|
||||
setFields.push('chunk_status = ?');
|
||||
params.push(getChunkStatusForSource(normalizedSource));
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(updates, 'context_id')) {
|
||||
setFields.push('context_id = ?');
|
||||
params.push(updates.context_id ?? null);
|
||||
}
|
||||
if (mergedMetadata !== undefined) {
|
||||
setFields.push('metadata = ?');
|
||||
params.push(JSON.stringify(mergedMetadata));
|
||||
@@ -304,7 +283,7 @@ function getNodeCount() {
|
||||
|
||||
/**
|
||||
* Get knowledge graph context overview.
|
||||
* Returns stats, contexts, hub nodes, and recent activity.
|
||||
* Returns stats, hub nodes, and recent activity.
|
||||
*/
|
||||
function getContext() {
|
||||
const nodeCount = query('SELECT COUNT(*) as count FROM nodes')[0].count;
|
||||
@@ -323,12 +302,11 @@ function getContext() {
|
||||
LEFT JOIN edges e ON n.id = e.from_node_id OR n.id = e.to_node_id
|
||||
GROUP BY n.id
|
||||
ORDER BY edge_count DESC
|
||||
LIMIT 5
|
||||
LIMIT 10
|
||||
`);
|
||||
|
||||
return {
|
||||
stats: { nodeCount, edgeCount, dimensionCount: 0, contextCount: contextService.listContexts().length },
|
||||
contexts: contextService.listContexts(),
|
||||
stats: { nodeCount, edgeCount, dimensionCount: 0 },
|
||||
recentNodes,
|
||||
hubNodes
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
const { getDb, query } = require('./sqlite-client');
|
||||
const nodeService = require('./nodeService');
|
||||
const edgeService = require('./edgeService');
|
||||
const contextService = require('./contextService');
|
||||
|
||||
const LOW_SIGNAL_PATTERNS = [
|
||||
/^(yes|yeah|yep|no|nope|nah|ok|okay|cool|great|nice|thanks|thank you|sure|sounds good|go ahead|do it)[.!]?$/i,
|
||||
@@ -475,7 +474,6 @@ function searchChunks(queryText, nodeIds, limit) {
|
||||
function retrieveQueryContext(input = {}) {
|
||||
const queryText = normalizeWhitespace(input.query || '');
|
||||
const focusedNodeId = input.focused_node_id ?? null;
|
||||
const requestedActiveContextId = input.active_context_id ?? null;
|
||||
const limit = Math.min(Math.max(input.limit || 6, 1), 12);
|
||||
const shouldRetrieve = shouldRetrieveForQuery(queryText);
|
||||
|
||||
@@ -486,14 +484,11 @@ function retrieveQueryContext(input = {}) {
|
||||
mode: 'skip',
|
||||
reason: 'Query is too lightweight or conversational to justify retrieval.',
|
||||
focused_node_id: focusedNodeId,
|
||||
active_context_id: requestedActiveContextId,
|
||||
nodes: [],
|
||||
chunks: [],
|
||||
};
|
||||
}
|
||||
|
||||
const activeContext = requestedActiveContextId ? contextService.getContextById(requestedActiveContextId) : null;
|
||||
const activeContextId = activeContext ? activeContext.id : null;
|
||||
const focusedRequest = isFocusedSourceRequest(queryText);
|
||||
const directNodeRetrieval = isDirectNodeRetrievalQuery(queryText);
|
||||
const nodesById = new Map();
|
||||
@@ -527,19 +522,6 @@ function retrieveQueryContext(input = {}) {
|
||||
});
|
||||
});
|
||||
|
||||
if (activeContextId && !strongRecallMatch) {
|
||||
const contextMatches = queryText
|
||||
? nodeService.getNodes({ search: queryText, contextId: activeContextId, limit: Math.max(limit, 4) })
|
||||
: [];
|
||||
contextMatches.forEach((node, index) => {
|
||||
addNodeWithReason(nodesById, node, {
|
||||
kind: 'context_hint',
|
||||
reason: 'Also matched inside the active context.',
|
||||
searchRank: directQueryMatches.length + index,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!strongRecallMatch) {
|
||||
const rankedSeedNodes = rankRetrievedNodes(Array.from(nodesById.values())).slice(0, Math.max(3, limit));
|
||||
rankedSeedNodes.slice(0, 3).forEach((seed) => {
|
||||
@@ -583,7 +565,6 @@ function retrieveQueryContext(input = {}) {
|
||||
? 'Direct node retrieval query: search the graph directly first and broaden only if needed.'
|
||||
: 'Substantive query: search the graph directly, then pull additional supporting context if helpful.',
|
||||
focused_node_id: focusedNodeId,
|
||||
active_context_id: activeContextId,
|
||||
nodes: finalNodes,
|
||||
chunks,
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ function getExistingColumnNames(db, tableName) {
|
||||
}
|
||||
|
||||
function validateExistingRahSchema(db) {
|
||||
const requiredTables = ['contexts', 'nodes', 'edges', 'chunks'];
|
||||
const requiredTables = ['nodes', 'edges', 'chunks'];
|
||||
const missingTables = requiredTables.filter(
|
||||
tableName => !db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(tableName)
|
||||
);
|
||||
@@ -46,16 +46,14 @@ function validateExistingRahSchema(db) {
|
||||
|
||||
const requiredNodeColumns = [
|
||||
'title', 'description', 'source', 'link', 'event_date', 'metadata',
|
||||
'embedding', 'embedding_updated_at', 'embedding_text', 'chunk_status', 'context_id',
|
||||
'embedding', 'embedding_updated_at', 'embedding_text', 'chunk_status',
|
||||
'created_at', 'updated_at'
|
||||
];
|
||||
const requiredContextColumns = ['name', 'description', 'icon', 'created_at', 'updated_at'];
|
||||
const requiredEdgeColumns = ['from_node_id', 'to_node_id', 'source', 'created_at', 'context', 'explanation'];
|
||||
const requiredChunkColumns = ['node_id', 'chunk_idx', 'text', 'embedding_type', 'metadata', 'created_at'];
|
||||
|
||||
const schemaChecks = [
|
||||
['nodes', requiredNodeColumns],
|
||||
['contexts', requiredContextColumns],
|
||||
['edges', requiredEdgeColumns],
|
||||
['chunks', requiredChunkColumns],
|
||||
];
|
||||
@@ -104,15 +102,6 @@ function initDatabase() {
|
||||
|
||||
function ensureCoreSchema(db) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS contexts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
icon TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title TEXT,
|
||||
@@ -126,13 +115,8 @@ function ensureCoreSchema(db) {
|
||||
embedding BLOB,
|
||||
embedding_updated_at TEXT,
|
||||
embedding_text TEXT,
|
||||
chunk_status TEXT DEFAULT 'not_chunked',
|
||||
context_id INTEGER,
|
||||
FOREIGN KEY (context_id) REFERENCES contexts(id) ON DELETE SET NULL
|
||||
chunk_status TEXT DEFAULT 'not_chunked'
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_contexts_name_normalized
|
||||
ON contexts(LOWER(TRIM(name)));
|
||||
`);
|
||||
|
||||
ensureEdgesTableSchema(db);
|
||||
|
||||
@@ -9,7 +9,7 @@ description: "Use for structured review, QA, cleanup, or governance checks acros
|
||||
|
||||
1. Node quality: duplicates, vague descriptions, missing dates, weak titles.
|
||||
2. Edge quality: missing links, weak explanations, wrong directionality.
|
||||
3. Context quality: drift, overlap, low-signal buckets, or contexts being overused where stronger node metadata should carry the meaning.
|
||||
3. Graph structure quality: weak clustering, redundant organizational nodes, or cases where stronger titles, descriptions, metadata, and edges should carry the meaning.
|
||||
4. Skill quality: trigger clarity, overlap, dead/unused skills.
|
||||
|
||||
## Output Format
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Calibration
|
||||
description: "Run periodic check-ins to recalibrate goals, projects, preferences, and graph structure."
|
||||
when_to_use: "User asks for a check-in, reset, review, or strategic recalibration."
|
||||
when_not_to_use: "Single isolated question with no strategic update needed."
|
||||
success_criteria: "Graph reflects current reality: updated contexts, changed priorities, and explicit deltas."
|
||||
success_criteria: "Graph reflects current reality: updated nodes, changed priorities, and explicit deltas."
|
||||
---
|
||||
|
||||
# Calibration
|
||||
@@ -14,7 +14,7 @@ Re-anchor the graph to the user's current state.
|
||||
|
||||
## Check-in Sequence
|
||||
|
||||
1. Review major contexts, anchor nodes, and active project nodes.
|
||||
1. Review the strongest active nodes first.
|
||||
2. Ask what changed: goals, priorities, constraints, beliefs, preferences.
|
||||
3. Identify stale nodes and missing nodes.
|
||||
4. Propose precise updates: update existing vs create new.
|
||||
|
||||
@@ -13,7 +13,7 @@ description: "Use for graph read, write, connect, classify, or traverse operatio
|
||||
4. Search before create to avoid duplicates.
|
||||
5. Every create/update must include a natural description that clearly says what the thing is, why it matters here, and its current workflow status.
|
||||
6. Use event dates when known (when it happened, not when saved).
|
||||
7. Leave context blank by default. Only apply context when the user explicitly wants it or when one obvious existing context is clearly useful. One node gets at most one primary context, and leaving context blank is valid.
|
||||
7. Leave extra taxonomy out of normal writes. Good nodes and good edges should carry the structure.
|
||||
8. Do not create edges autonomously. Surface likely edge candidates first, then create them only after the user explicitly confirms.
|
||||
9. For user-authored ideas, notes, or dictated thoughts, preserve the user's wording in `source` as fully as possible with only minimal cleanup.
|
||||
|
||||
@@ -24,9 +24,6 @@ description: "Use for graph read, write, connect, classify, or traverse operatio
|
||||
- `source`: full verbatim or canonical content of the node (transcript, article text, book passage, user's thoughts). The standalone MCP server stores this on the node. The RA-H app later chunks and embeds it for semantic search.
|
||||
- For idea capture from chat, the `source` should usually be the raw user thought, not a compressed assistant summary.
|
||||
- `link`: external source URL only.
|
||||
- Normal writes should omit context entirely unless the user explicitly wants one.
|
||||
- If context is intentionally provided, prefer `context_name`.
|
||||
- Treat numeric `context_id` as an internal implementation detail, not a normal agent-facing field.
|
||||
- `metadata`: use the canonical node metadata contract when metadata is needed:
|
||||
- `type`
|
||||
- `state` (`processed` or `not_processed`)
|
||||
@@ -48,7 +45,7 @@ It must still make three things clear:
|
||||
2. Why — why it is in the graph; what Brad is interested in; what it connects to
|
||||
3. Status — where it sits in his workflow (queued, in progress, processed, unknown)
|
||||
|
||||
If the agent has graph context (context capsule, focused nodes, recent connected nodes, or an explicit active context), it should infer the why from that context and write it naturally. Do not let the service auto-generate a weak context-free description when you already have enough signal.
|
||||
If the agent has graph context (focused nodes, recent connected nodes, or nearby graph structure), it should infer the why from that context and write it naturally. Do not let the service auto-generate a weak context-free description when you already have enough signal.
|
||||
|
||||
If the why genuinely cannot be inferred, say that naturally. Do not use labels like `WHAT:`, `WHY:`, or `STATUS:` and do not substitute vague filler like `insightful for understanding` or `relevant to Brad's work`.
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
name: Node Context Enrichment
|
||||
description: "Use to rewrite thin node descriptions into natural prose that still makes what, why, and status clear, with context review and edge suggestions."
|
||||
description: "Use to rewrite thin node descriptions into natural prose that still makes what, why, and status clear, with graph review and edge suggestions."
|
||||
---
|
||||
|
||||
# Node Context Enrichment
|
||||
|
||||
Use this when a node already exists but its description is thin, generic, or missing personal context.
|
||||
Use this when a node already exists but its description is thin, generic, or missing useful graph framing.
|
||||
|
||||
This skill should not silently rewrite and move on when contextual framing is inferred. If the enrichment depends on interpretation, update the node and then explicitly invite the user to correct or refine the contextual framing.
|
||||
This skill should not silently rewrite and move on when framing is inferred. If the enrichment depends on interpretation, update the node and then explicitly invite the user to correct or refine that framing.
|
||||
|
||||
## Goal
|
||||
|
||||
@@ -17,24 +17,19 @@ Replace weak descriptions with a single clean natural description that captures:
|
||||
2. Why it is in Brad's graph
|
||||
3. Status in Brad's workflow
|
||||
|
||||
Also review whether the node needs context cleanup or obvious edge suggestions.
|
||||
Also review whether the node needs obvious edge suggestions.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Load the node and inspect title, description, source, link, metadata, context, and nearby edges.
|
||||
2. Search for adjacent context before rewriting:
|
||||
- the node's primary context and its anchor node when present
|
||||
1. Load the node and inspect title, description, source, link, metadata, and nearby edges.
|
||||
2. Search for adjacent graph context before rewriting:
|
||||
- recently connected project or belief nodes
|
||||
- related nodes with overlapping titles, creators, or neighboring context
|
||||
3. Infer the best available "why" from that context.
|
||||
- related nodes with overlapping titles, creators, or neighboring structure
|
||||
3. Infer the best available "why" from that graph context.
|
||||
4. Rewrite the full description from scratch in natural prose. Do not append to the old text or use labels like WHAT:, WHY:, or STATUS:.
|
||||
5. Review context fit:
|
||||
- keep the current context when it is clearly the primary scope
|
||||
- suggest clearing context when it is weak or misleading
|
||||
- suggest a different context only when the primary scope is explicit
|
||||
6. Suggest 1-3 high-signal edges when obvious.
|
||||
7. Update the node once the description is strong enough to be useful.
|
||||
8. After the update, tell the user what changed and ask whether they want to refine the important framing:
|
||||
5. Suggest 1-3 high-signal edges when obvious.
|
||||
6. Update the node once the description is strong enough to be useful.
|
||||
7. After the update, tell the user what changed and ask whether they want to refine the important framing:
|
||||
- what it is
|
||||
- why it belongs in the graph
|
||||
- status / current relevance / workflow position
|
||||
@@ -52,7 +47,7 @@ Every rewritten description must naturally cover:
|
||||
2. Why
|
||||
- why Brad saved it
|
||||
- what project, belief, question, or theme it connects to
|
||||
- if genuinely unknown, say that naturally without inventing context
|
||||
- if genuinely unknown, say that naturally without inventing graph framing
|
||||
3. Status
|
||||
- queued, in progress, processed, not yet reviewed, saved for later, etc.
|
||||
- if unknown, say naturally that it has not been reviewed yet
|
||||
@@ -71,14 +66,13 @@ Use batch enrichment when cleaning up many nodes with the same failure mode.
|
||||
3. Enrich each node individually. Do not reuse boilerplate "why" text across unrelated nodes.
|
||||
4. Return a compact summary of:
|
||||
- nodes updated
|
||||
- context assignments to review
|
||||
- edge suggestions not yet created
|
||||
- edge suggestions not yet created
|
||||
|
||||
## Quality Bar
|
||||
|
||||
- No filler phrases like `insightful for understanding`, `relevant to`, or `important for`.
|
||||
- No generic summaries that only restate the topic.
|
||||
- No invented certainty. If context is weak, say so explicitly.
|
||||
- No invented certainty. If graph evidence is weak, say so explicitly.
|
||||
- Prefer one compact 3-sentence description over bloated prose.
|
||||
|
||||
## Output Pattern
|
||||
@@ -86,6 +80,6 @@ Use batch enrichment when cleaning up many nodes with the same failure mode.
|
||||
For each node:
|
||||
|
||||
- New description
|
||||
- Context change: keep / change / clear
|
||||
- Framing note: what graph context influenced the rewrite, if any
|
||||
- Edge suggestions: source -> target with explicit explanation
|
||||
- One short invitation for user feedback when contextual framing was inferred
|
||||
- One short invitation for user feedback when framing was inferred
|
||||
|
||||
@@ -7,7 +7,7 @@ description: "Use for new-user setup, empty or near-empty graphs, or major reset
|
||||
|
||||
## Your Job
|
||||
|
||||
Three things: help the user understand the basic structure of the system, help them start building useful graph data in it, and bootstrap the context capsule when durable cross-session facts become clear.
|
||||
Three things: help the user understand the basic structure of the system, help them start building useful graph data in it, and get them to a useful starter graph quickly.
|
||||
|
||||
Adapt to the user.
|
||||
|
||||
@@ -30,28 +30,6 @@ Only bring up setup details if the user actually needs them:
|
||||
- **Anthropic** — mainly relevant for compatible runtime paths and local/dev setups.
|
||||
4. If they are not ready to configure anything yet, keep onboarding. They can still learn the structure and add manual content.
|
||||
|
||||
## Capsule Bootstrap
|
||||
|
||||
As part of onboarding, also bootstrap the context capsule on the user's behalf.
|
||||
|
||||
When the user gives durable identity, preference, worldview, project, or agent-behavior information:
|
||||
|
||||
1. Call `readSkill('context-capsule')`
|
||||
2. Call `readCapsule`
|
||||
3. When you have enough confidence, call `writeCapsule`
|
||||
|
||||
Use the capsule for:
|
||||
- preferred name
|
||||
- agent name or greeting preference
|
||||
- interaction style
|
||||
- major projects
|
||||
- major interests
|
||||
- explicit worldview or belief signals
|
||||
- what the user is using RA-H for
|
||||
|
||||
Do not write the capsule for tentative, one-off, or low-confidence statements.
|
||||
Keep it compressed and canonical. Replace stale instructions instead of preserving contradictory history.
|
||||
|
||||
## Explain the System First
|
||||
|
||||
Before asking anything, orient the user. Be direct, not salesy:
|
||||
@@ -60,7 +38,6 @@ Before asking anything, orient the user. Be direct, not salesy:
|
||||
|
||||
Explain the structure in simple terms:
|
||||
|
||||
- **Contexts** — an optional soft organization layer. These are broad areas like health, job, life, or research. A node can belong to one context when it is explicit and useful, but context is not required.
|
||||
- **Nodes** — individual things. A project, idea, person, source, belief, decision, or topic. Each node must have a clear description of what it is and why it matters.
|
||||
- **Edges** — explicit connections between things. Each edge must clearly explain the relationship.
|
||||
- **Metadata and edges** — secondary structure that makes nodes more useful once the core artifact is clear.
|
||||
@@ -71,7 +48,7 @@ Then say:
|
||||
|
||||
Also explain one practical thing early:
|
||||
|
||||
> "You do not need to perfectly design this up front. We want a few concrete nodes, clear contexts when they matter, and a few clean edges so the graph becomes useful quickly."
|
||||
> "You do not need to perfectly design this up front. We want a few concrete nodes and a few clean edges so the graph becomes useful quickly."
|
||||
|
||||
## Interview Flow
|
||||
|
||||
@@ -101,7 +78,6 @@ Keep it conversational. Use these buckets and adapt based on what the user gives
|
||||
Work these in naturally when they are relevant:
|
||||
|
||||
- **First node creation** — explain that a node is one concrete thing worth keeping: a project, source, person, belief, decision, or idea.
|
||||
- **Contexts** — explain that contexts are optional helpers, not required setup. If the user has none, that is fine.
|
||||
- **MCP connection** — if the user mentions Claude Code or external agents, offer a quick setup path and point them to the MCP docs/skill flow rather than reciting a giant config block immediately.
|
||||
- **What to do after setup** — once the graph has a few solid nodes, the next useful move is usually one of:
|
||||
- connect related nodes with explicit edges
|
||||
@@ -113,7 +89,6 @@ Work these in naturally when they are relevant:
|
||||
Do your best to build the graph as useful context emerges.
|
||||
|
||||
- Add nodes when the user mentions concrete things worth keeping.
|
||||
- Assign a context only when it is an obvious match to one of the user's existing contexts. Prefer leaving context empty over low-confidence guessing.
|
||||
- Add edges when relationships are clear enough to explain well.
|
||||
- Explain what you're adding in plain language so the user understands the structure as it develops.
|
||||
|
||||
@@ -125,22 +100,16 @@ Before writing anything, call `readSkill('db-operations')` for full quality stan
|
||||
|
||||
- Search before creating — avoid duplicates from day one
|
||||
- Every description must be concrete: what it IS and why it matters to them, not what it "explores" or "discusses"
|
||||
- Contexts are optional and should only be used for an obvious existing match; otherwise leave them empty
|
||||
- Every edge needs an explicit explanation sentence
|
||||
|
||||
## Propose Before Writing
|
||||
|
||||
When there is enough context, summarize the proposed structure before touching the database:
|
||||
|
||||
> "Here's what I'm planning to create: [list contexts with one-line rationale], [list starter nodes], [list key edges]. Does this look right? Anything to adjust?"
|
||||
> "Here's what I'm planning to create: [list starter nodes], [list key edges]. Does this look right? Anything to adjust?"
|
||||
|
||||
Write only after confirmation.
|
||||
|
||||
If onboarding also surfaced durable cross-session facts, include the capsule update in the proposal:
|
||||
|
||||
> "I also plan to bootstrap your context capsule with your name, interaction preferences, and current priorities so future chats start grounded. Sound right?"
|
||||
> "I also plan to bootstrap your context capsule with your name and interaction preferences so future chats start grounded. Sound right?"
|
||||
|
||||
For very early setup, include the first actionable next step too:
|
||||
|
||||
> "After this starter pass, the best next move will be [add a source / connect these nodes / capture another active project]."
|
||||
|
||||
Reference in New Issue
Block a user